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:
@@ -246,7 +246,9 @@
|
|||||||
|
|
||||||
Pokemon 可配置:
|
Pokemon 可配置:
|
||||||
|
|
||||||
- ID
|
- 内部 ID:`id`,系统唯一,用于路由、外键和实体关联;普通 Pokemon 新建时优先与展示 ID 一致,活动 Pokemon 由系统分配唯一内部 ID
|
||||||
|
- 展示 ID:`display_id`,详情页、列表卡片和选择器中显示为 `#ID`
|
||||||
|
- 是否为活动物品:`is_event_item`
|
||||||
- 名称
|
- 名称
|
||||||
- Genus:可为空,支持翻译
|
- Genus:可为空,支持翻译
|
||||||
- 介绍 / Details:可为空,支持翻译
|
- 介绍 / Details:可为空,支持翻译
|
||||||
@@ -269,6 +271,8 @@ Pokemon 可配置:
|
|||||||
- 翻译
|
- 翻译
|
||||||
- 排序
|
- 排序
|
||||||
|
|
||||||
|
Pokemon 的展示 ID 在普通 Pokemon 和活动 Pokemon 之间可以重复,例如允许同时存在普通 `#1 妙蛙种子` 和活动 `#1 毽子草`。数据库只要求同一个 `display_id + is_event_item` 组合唯一;前端路由和实体关联必须继续使用内部 `id`,不能使用展示 ID 作为路由或外键。
|
||||||
|
|
||||||
Pokemon 编辑表单使用标签页组织字段:
|
Pokemon 编辑表单使用标签页组织字段:
|
||||||
|
|
||||||
- 编辑表单提供 Fetch data 功能:
|
- 编辑表单提供 Fetch data 功能:
|
||||||
@@ -341,6 +345,7 @@ Pokemon 详情页展示:
|
|||||||
物品可配置:
|
物品可配置:
|
||||||
|
|
||||||
- 名称
|
- 名称
|
||||||
|
- 是否为活动物品:`is_event_item`
|
||||||
- 分类:必填
|
- 分类:必填
|
||||||
- 用途:可为空
|
- 用途:可为空
|
||||||
- 入手方式:可多选
|
- 入手方式:可多选
|
||||||
@@ -423,6 +428,7 @@ Pokemon 详情页展示:
|
|||||||
栖息地可配置:
|
栖息地可配置:
|
||||||
|
|
||||||
- 名称
|
- 名称
|
||||||
|
- 是否为活动物品:`is_event_item`
|
||||||
- 配方:多项物品 + 数量
|
- 配方:多项物品 + 数量
|
||||||
- 可出现的 Pokemon
|
- 可出现的 Pokemon
|
||||||
- 图片:通过通用 Wiki 图片上传维护当前图片和历史上传记录
|
- 图片:通过通用 Wiki 图片上传维护当前图片和历史上传记录
|
||||||
|
|||||||
@@ -263,7 +263,9 @@ CREATE TABLE IF NOT EXISTS pokemon_types (
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS pokemon (
|
CREATE TABLE IF NOT EXISTS pokemon (
|
||||||
id integer PRIMARY KEY,
|
id integer PRIMARY KEY,
|
||||||
|
display_id integer NOT NULL CHECK (display_id > 0),
|
||||||
name text NOT NULL UNIQUE,
|
name text NOT NULL UNIQUE,
|
||||||
|
is_event_item boolean NOT NULL DEFAULT false,
|
||||||
genus text NOT NULL DEFAULT '',
|
genus text NOT NULL DEFAULT '',
|
||||||
details text NOT NULL DEFAULT '',
|
details text NOT NULL DEFAULT '',
|
||||||
height_inches double precision NOT NULL DEFAULT 0 CHECK (height_inches >= 0),
|
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,
|
dual_dyeable boolean NOT NULL DEFAULT false,
|
||||||
pattern_editable boolean NOT NULL DEFAULT false,
|
pattern_editable boolean NOT NULL DEFAULT false,
|
||||||
no_recipe 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 '',
|
image_path text NOT NULL DEFAULT '',
|
||||||
sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0)
|
sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0)
|
||||||
);
|
);
|
||||||
|
|
||||||
ALTER TABLE items ALTER COLUMN usage_id DROP NOT NULL;
|
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 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 ADD COLUMN IF NOT EXISTS image_path text NOT NULL DEFAULT '';
|
||||||
ALTER TABLE items DROP COLUMN IF EXISTS no_habitat;
|
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 (
|
CREATE TABLE IF NOT EXISTS habitats (
|
||||||
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||||
name text NOT NULL UNIQUE,
|
name text NOT NULL UNIQUE,
|
||||||
|
is_event_item boolean NOT NULL DEFAULT false,
|
||||||
image_path text NOT NULL DEFAULT '',
|
image_path text NOT NULL DEFAULT '',
|
||||||
sort_order integer NOT NULL DEFAULT 0 CHECK (sort_order >= 0)
|
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 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 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 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 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 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);
|
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 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 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 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 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;
|
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 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 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 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 '';
|
ALTER TABLE habitats ADD COLUMN IF NOT EXISTS image_path text NOT NULL DEFAULT '';
|
||||||
|
|
||||||
WITH ordered AS (
|
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 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_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 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 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_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);
|
CREATE INDEX IF NOT EXISTS item_usages_sort_order_idx ON item_usages(sort_order, id);
|
||||||
|
|||||||
@@ -101,7 +101,8 @@ type PokemonImageOptionsResult = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type PokemonPayload = {
|
type PokemonPayload = {
|
||||||
id: number;
|
displayId: number;
|
||||||
|
isEventItem: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
genus: string;
|
genus: string;
|
||||||
details: string;
|
details: string;
|
||||||
@@ -154,6 +155,7 @@ type ItemPayload = {
|
|||||||
dualDyeable: boolean;
|
dualDyeable: boolean;
|
||||||
patternEditable: boolean;
|
patternEditable: boolean;
|
||||||
noRecipe: boolean;
|
noRecipe: boolean;
|
||||||
|
isEventItem: boolean;
|
||||||
acquisitionMethodIds: number[];
|
acquisitionMethodIds: number[];
|
||||||
tagIds: number[];
|
tagIds: number[];
|
||||||
imagePath: string;
|
imagePath: string;
|
||||||
@@ -250,6 +252,7 @@ type LifePostsPage = {
|
|||||||
type HabitatPayload = {
|
type HabitatPayload = {
|
||||||
name: string;
|
name: string;
|
||||||
translations: TranslationInput;
|
translations: TranslationInput;
|
||||||
|
isEventItem: boolean;
|
||||||
imagePath: string;
|
imagePath: string;
|
||||||
recipeItems: IdQuantity[];
|
recipeItems: IdQuantity[];
|
||||||
pokemonAppearances: Array<{
|
pokemonAppearances: Array<{
|
||||||
@@ -283,6 +286,8 @@ type EditHistoryEntry = {
|
|||||||
user: { id: number; displayName: string } | null;
|
user: { id: number; displayName: string } | null;
|
||||||
};
|
};
|
||||||
type PokemonChangeSource = {
|
type PokemonChangeSource = {
|
||||||
|
displayId: number;
|
||||||
|
isEventItem: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
genus: string;
|
genus: string;
|
||||||
details: string;
|
details: string;
|
||||||
@@ -297,6 +302,7 @@ type PokemonChangeSource = {
|
|||||||
};
|
};
|
||||||
type ItemChangeSource = {
|
type ItemChangeSource = {
|
||||||
name: string;
|
name: string;
|
||||||
|
isEventItem: boolean;
|
||||||
image: EntityImageValue | null;
|
image: EntityImageValue | null;
|
||||||
category: { name: string };
|
category: { name: string };
|
||||||
usage: { name: string } | null;
|
usage: { name: string } | null;
|
||||||
@@ -307,6 +313,7 @@ type ItemChangeSource = {
|
|||||||
};
|
};
|
||||||
type HabitatChangeSource = {
|
type HabitatChangeSource = {
|
||||||
name: string;
|
name: string;
|
||||||
|
isEventItem: boolean;
|
||||||
image: EntityImageValue | null;
|
image: EntityImageValue | null;
|
||||||
recipe: Array<{ name: string; quantity: number }>;
|
recipe: Array<{ name: string; quantity: number }>;
|
||||||
pokemon: Array<{ name: string; time_of_day: string; weather: string; rarity: number; map: { name: string } }>;
|
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<numbe
|
|||||||
return result.rows[0]?.sortOrder ?? 10;
|
return result.rows[0]?.sortOrder ?? 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function nextPokemonInternalId(client: DbClient, displayId: number, isEventItem: boolean): Promise<number> {
|
||||||
|
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(
|
async function reorderTableRows(
|
||||||
client: DbClient,
|
client: DbClient,
|
||||||
tableName: string,
|
tableName: string,
|
||||||
@@ -1717,6 +1748,8 @@ async function pokemonEditChanges(
|
|||||||
.join(' / ');
|
.join(' / ');
|
||||||
|
|
||||||
pushChange(changes, 'Name', before.name, after.name);
|
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, 'Genus', before.genus, after.genus);
|
||||||
pushChange(changes, 'Details', before.details, after.details);
|
pushChange(changes, 'Details', before.details, after.details);
|
||||||
pushChange(changes, 'Height', pokemonHeightValue(before.heightInches), pokemonHeightValue(after.heightInches));
|
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);
|
const tagNames = await entityNameMap(client, 'favorite_things', after.tagIds);
|
||||||
|
|
||||||
pushChange(changes, 'Name', before.name, after.name);
|
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, 'Image', imagePathLabel(before.image?.path), imagePathLabel(after.imagePath));
|
||||||
pushChange(changes, 'Category', before.category.name, categoryNames.get(after.categoryId));
|
pushChange(changes, 'Category', before.category.name, categoryNames.get(after.categoryId));
|
||||||
pushChange(changes, 'Usage', before.usage?.name, after.usageId ? usageNames.get(after.usageId) : null);
|
pushChange(changes, 'Usage', before.usage?.name, after.usageId ? usageNames.get(after.usageId) : null);
|
||||||
@@ -1776,6 +1810,7 @@ async function habitatEditChanges(
|
|||||||
.join(' / ');
|
.join(' / ');
|
||||||
|
|
||||||
pushChange(changes, 'Name', before.name, after.name);
|
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, 'Image', imagePathLabel(before.image?.path), imagePathLabel(after.imagePath));
|
||||||
pushChange(changes, 'Recipe', quantityListValue(before.recipe), await quantityPayloadValue(client, after.recipeItems));
|
pushChange(changes, 'Recipe', quantityListValue(before.recipe), await quantityPayloadValue(client, after.recipeItems));
|
||||||
pushChange(changes, 'Possible Pokemon', appearanceListValue(before.pokemon), afterAppearances);
|
pushChange(changes, 'Possible Pokemon', appearanceListValue(before.pokemon), afterAppearances);
|
||||||
@@ -1832,8 +1867,10 @@ function pokemonProjection(locale: string): string {
|
|||||||
return `
|
return `
|
||||||
SELECT
|
SELECT
|
||||||
p.id,
|
p.id,
|
||||||
|
p.display_id AS "displayId",
|
||||||
${pokemonName} AS name,
|
${pokemonName} AS name,
|
||||||
p.name AS "baseName",
|
p.name AS "baseName",
|
||||||
|
p.is_event_item AS "isEventItem",
|
||||||
${pokemonGenus} AS genus,
|
${pokemonGenus} AS genus,
|
||||||
p.genus AS "baseGenus",
|
p.genus AS "baseGenus",
|
||||||
${pokemonDetails} AS details,
|
${pokemonDetails} AS details,
|
||||||
@@ -3117,7 +3154,9 @@ export async function getPokemon(id: number, locale = defaultLocale) {
|
|||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
related_pokemon.id,
|
related_pokemon.id,
|
||||||
|
related_pokemon.display_id AS "displayId",
|
||||||
${relatedPokemonName} AS name,
|
${relatedPokemonName} AS name,
|
||||||
|
related_pokemon.is_event_item AS "isEventItem",
|
||||||
${pokemonImageJson('related_pokemon')} AS image,
|
${pokemonImageJson('related_pokemon')} AS image,
|
||||||
json_build_object('id', related_environment.id, 'name', ${relatedEnvironmentName}) AS environment,
|
json_build_object('id', related_environment.id, 'name', ${relatedEnvironmentName}) AS environment,
|
||||||
COALESCE((
|
COALESCE((
|
||||||
@@ -3215,10 +3254,11 @@ function cleanPokemonPayload(payload: Record<string, unknown>): PokemonPayload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = requirePositiveInteger(payload.id, 'Pokemon ID is required');
|
const displayId = requirePositiveInteger(payload.displayId ?? payload.id, 'Pokemon ID is required');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
displayId,
|
||||||
|
isEventItem: Boolean(payload.isEventItem),
|
||||||
name: cleanName(payload.name, 'Pokemon name is required'),
|
name: cleanName(payload.name, 'Pokemon name is required'),
|
||||||
genus: cleanOptionalText(payload.genus),
|
genus: cleanOptionalText(payload.genus),
|
||||||
details: cleanOptionalText(payload.details),
|
details: cleanOptionalText(payload.details),
|
||||||
@@ -3231,7 +3271,7 @@ function cleanPokemonPayload(payload: Record<string, unknown>): PokemonPayload {
|
|||||||
skillIds,
|
skillIds,
|
||||||
favoriteThingIds,
|
favoriteThingIds,
|
||||||
skillItemDrops: [...skillItemDrops.values()],
|
skillItemDrops: [...skillItemDrops.values()],
|
||||||
image: cleanPokemonImage(payload.imagePath, id)
|
image: cleanPokemonImage(payload.imagePath, displayId)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3284,12 +3324,15 @@ export async function createPokemon(payload: Record<string, unknown>, userId: nu
|
|||||||
const cleanPayload = cleanPokemonPayload(payload);
|
const cleanPayload = cleanPokemonPayload(payload);
|
||||||
|
|
||||||
const id = await withTransaction(async (client) => {
|
const id = await withTransaction(async (client) => {
|
||||||
|
const pokemonId = await nextPokemonInternalId(client, cleanPayload.displayId, cleanPayload.isEventItem);
|
||||||
const sortOrder = await nextSortOrder(client, 'pokemon');
|
const sortOrder = await nextSortOrder(client, 'pokemon');
|
||||||
await client.query(
|
await client.query(
|
||||||
`
|
`
|
||||||
INSERT INTO pokemon (
|
INSERT INTO pokemon (
|
||||||
id,
|
id,
|
||||||
|
display_id,
|
||||||
name,
|
name,
|
||||||
|
is_event_item,
|
||||||
genus,
|
genus,
|
||||||
details,
|
details,
|
||||||
height_inches,
|
height_inches,
|
||||||
@@ -3310,11 +3353,13 @@ export async function createPokemon(payload: Record<string, unknown>, userId: nu
|
|||||||
created_by_user_id,
|
created_by_user_id,
|
||||||
updated_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.name,
|
||||||
|
cleanPayload.isEventItem,
|
||||||
cleanPayload.genus,
|
cleanPayload.genus,
|
||||||
cleanPayload.details,
|
cleanPayload.details,
|
||||||
cleanPayload.heightInches,
|
cleanPayload.heightInches,
|
||||||
@@ -3335,17 +3380,17 @@ export async function createPokemon(payload: Record<string, unknown>, userId: nu
|
|||||||
userId
|
userId
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
await linkEntityImageUpload(client, 'pokemon', cleanPayload.id, cleanPayload.image?.path, cleanPayload.name);
|
await linkEntityImageUpload(client, 'pokemon', pokemonId, cleanPayload.image?.path, cleanPayload.name);
|
||||||
await replacePokemonRelations(client, cleanPayload.id, cleanPayload);
|
await replacePokemonRelations(client, pokemonId, cleanPayload);
|
||||||
await replaceEntityTranslations(client, 'pokemon', cleanPayload.id, cleanPayload.translations, ['name', 'details', 'genus']);
|
await replaceEntityTranslations(client, 'pokemon', pokemonId, cleanPayload.translations, ['name', 'details', 'genus']);
|
||||||
await recordEditLog(client, 'pokemon', cleanPayload.id, 'create', userId);
|
await recordEditLog(client, 'pokemon', pokemonId, 'create', userId);
|
||||||
return cleanPayload.id;
|
return pokemonId;
|
||||||
});
|
});
|
||||||
return getPokemon(id, locale);
|
return getPokemon(id, locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updatePokemon(id: number, payload: Record<string, unknown>, userId: number, locale = defaultLocale) {
|
export async function updatePokemon(id: number, payload: Record<string, unknown>, userId: number, locale = defaultLocale) {
|
||||||
const cleanPayload = cleanPokemonPayload({ ...payload, id });
|
const cleanPayload = cleanPokemonPayload(payload);
|
||||||
const before = await getPokemon(id, defaultLocale);
|
const before = await getPokemon(id, defaultLocale);
|
||||||
|
|
||||||
const updated = await withTransaction(async (client) => {
|
const updated = await withTransaction(async (client) => {
|
||||||
@@ -3353,29 +3398,33 @@ export async function updatePokemon(id: number, payload: Record<string, unknown>
|
|||||||
`
|
`
|
||||||
UPDATE pokemon
|
UPDATE pokemon
|
||||||
SET
|
SET
|
||||||
name = $1,
|
display_id = $1,
|
||||||
genus = $2,
|
name = $2,
|
||||||
details = $3,
|
is_event_item = $3,
|
||||||
height_inches = $4,
|
genus = $4,
|
||||||
weight_pounds = $5,
|
details = $5,
|
||||||
environment_id = $6,
|
height_inches = $6,
|
||||||
hp = $7,
|
weight_pounds = $7,
|
||||||
attack = $8,
|
environment_id = $8,
|
||||||
defense = $9,
|
hp = $9,
|
||||||
special_attack = $10,
|
attack = $10,
|
||||||
special_defense = $11,
|
defense = $11,
|
||||||
speed = $12,
|
special_attack = $12,
|
||||||
image_path = $13,
|
special_defense = $13,
|
||||||
image_style = $14,
|
speed = $14,
|
||||||
image_version = $15,
|
image_path = $15,
|
||||||
image_variant = $16,
|
image_style = $16,
|
||||||
image_description = $17,
|
image_version = $17,
|
||||||
updated_by_user_id = $18,
|
image_variant = $18,
|
||||||
|
image_description = $19,
|
||||||
|
updated_by_user_id = $20,
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
WHERE id = $19
|
WHERE id = $21
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
|
cleanPayload.displayId,
|
||||||
cleanPayload.name,
|
cleanPayload.name,
|
||||||
|
cleanPayload.isEventItem,
|
||||||
cleanPayload.genus,
|
cleanPayload.genus,
|
||||||
cleanPayload.details,
|
cleanPayload.details,
|
||||||
cleanPayload.heightInches,
|
cleanPayload.heightInches,
|
||||||
@@ -3433,6 +3482,7 @@ export async function listHabitats(locale = defaultLocale) {
|
|||||||
h.id,
|
h.id,
|
||||||
${habitatName} AS name,
|
${habitatName} AS name,
|
||||||
h.name AS "baseName",
|
h.name AS "baseName",
|
||||||
|
h.is_event_item AS "isEventItem",
|
||||||
${translationsSelect('habitats', 'h.id')} AS translations,
|
${translationsSelect('habitats', 'h.id')} AS translations,
|
||||||
${auditSelect('h', 'habitat_created_user', 'habitat_updated_user')},
|
${auditSelect('h', 'habitat_created_user', 'habitat_updated_user')},
|
||||||
${uploadedImageJson('h.image_path')} AS image,
|
${uploadedImageJson('h.image_path')} AS image,
|
||||||
@@ -3443,9 +3493,17 @@ export async function listHabitats(locale = defaultLocale) {
|
|||||||
WHERE hri.habitat_id = h.id
|
WHERE hri.habitat_id = h.id
|
||||||
), '[]'::json) AS recipe,
|
), '[]'::json) AS recipe,
|
||||||
COALESCE((
|
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 (
|
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
|
FROM habitat_pokemon hp
|
||||||
JOIN pokemon p ON p.id = hp.pokemon_id
|
JOIN pokemon p ON p.id = hp.pokemon_id
|
||||||
WHERE hp.habitat_id = h.id
|
WHERE hp.habitat_id = h.id
|
||||||
@@ -3469,6 +3527,7 @@ export async function getHabitat(id: number, locale = defaultLocale) {
|
|||||||
h.id,
|
h.id,
|
||||||
${habitatName} AS name,
|
${habitatName} AS name,
|
||||||
h.name AS "baseName",
|
h.name AS "baseName",
|
||||||
|
h.is_event_item AS "isEventItem",
|
||||||
${translationsSelect('habitats', 'h.id')} AS translations,
|
${translationsSelect('habitats', 'h.id')} AS translations,
|
||||||
${auditSelect('h', 'habitat_created_user', 'habitat_updated_user')},
|
${auditSelect('h', 'habitat_created_user', 'habitat_updated_user')},
|
||||||
${uploadedImageJson('h.image_path')} AS image,
|
${uploadedImageJson('h.image_path')} AS image,
|
||||||
@@ -3502,7 +3561,9 @@ export async function getHabitat(id: number, locale = defaultLocale) {
|
|||||||
`
|
`
|
||||||
SELECT
|
SELECT
|
||||||
p.id,
|
p.id,
|
||||||
|
p.display_id AS "displayId",
|
||||||
${pokemonName} AS name,
|
${pokemonName} AS name,
|
||||||
|
p.is_event_item AS "isEventItem",
|
||||||
${pokemonImageJson('p')} AS image,
|
${pokemonImageJson('p')} AS image,
|
||||||
hp.time_of_day,
|
hp.time_of_day,
|
||||||
hp.weather,
|
hp.weather,
|
||||||
@@ -3557,6 +3618,7 @@ function cleanHabitatPayload(payload: Record<string, unknown>): HabitatPayload {
|
|||||||
return {
|
return {
|
||||||
name: cleanName(payload.name, 'Habitat name is required'),
|
name: cleanName(payload.name, 'Habitat name is required'),
|
||||||
translations: cleanTranslations(payload.translations, ['name']),
|
translations: cleanTranslations(payload.translations, ['name']),
|
||||||
|
isEventItem: Boolean(payload.isEventItem),
|
||||||
imagePath: cleanUploadImagePath(payload.imagePath, 'habitats'),
|
imagePath: cleanUploadImagePath(payload.imagePath, 'habitats'),
|
||||||
recipeItems: cleanQuantities(payload.recipeItems),
|
recipeItems: cleanQuantities(payload.recipeItems),
|
||||||
pokemonAppearances: [...pokemonAppearances.values()]
|
pokemonAppearances: [...pokemonAppearances.values()]
|
||||||
@@ -3593,11 +3655,11 @@ export async function createHabitat(payload: Record<string, unknown>, userId: nu
|
|||||||
const sortOrder = await nextSortOrder(client, 'habitats');
|
const sortOrder = await nextSortOrder(client, 'habitats');
|
||||||
const result = await client.query<{ id: number }>(
|
const result = await client.query<{ id: number }>(
|
||||||
`
|
`
|
||||||
INSERT INTO habitats (name, image_path, sort_order, created_by_user_id, updated_by_user_id)
|
INSERT INTO habitats (name, is_event_item, image_path, sort_order, created_by_user_id, updated_by_user_id)
|
||||||
VALUES ($1, $2, $3, $4, $4)
|
VALUES ($1, $2, $3, $4, $5, $5)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`,
|
`,
|
||||||
[cleanPayload.name, cleanPayload.imagePath, sortOrder, userId]
|
[cleanPayload.name, cleanPayload.isEventItem, cleanPayload.imagePath, sortOrder, userId]
|
||||||
);
|
);
|
||||||
const habitatId = result.rows[0].id;
|
const habitatId = result.rows[0].id;
|
||||||
await linkEntityImageUpload(client, 'habitats', habitatId, cleanPayload.imagePath, cleanPayload.name);
|
await linkEntityImageUpload(client, 'habitats', habitatId, cleanPayload.imagePath, cleanPayload.name);
|
||||||
@@ -3615,8 +3677,8 @@ export async function updateHabitat(id: number, payload: Record<string, unknown>
|
|||||||
|
|
||||||
const updated = await withTransaction(async (client) => {
|
const updated = await withTransaction(async (client) => {
|
||||||
const result = await client.query(
|
const result = await client.query(
|
||||||
'UPDATE habitats SET name = $1, image_path = $2, updated_by_user_id = $3, updated_at = now() WHERE id = $4',
|
'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.imagePath, userId, id]
|
[cleanPayload.name, cleanPayload.isEventItem, cleanPayload.imagePath, userId, id]
|
||||||
);
|
);
|
||||||
if (result.rowCount === 0) {
|
if (result.rowCount === 0) {
|
||||||
return false;
|
return false;
|
||||||
@@ -3656,6 +3718,7 @@ function itemProjection(locale: string): string {
|
|||||||
i.id,
|
i.id,
|
||||||
${itemName} AS name,
|
${itemName} AS name,
|
||||||
i.name AS "baseName",
|
i.name AS "baseName",
|
||||||
|
i.is_event_item AS "isEventItem",
|
||||||
${translationsSelect('items', 'i.id')} AS translations,
|
${translationsSelect('items', 'i.id')} AS translations,
|
||||||
${auditSelect('i', 'item_created_user', 'item_updated_user')},
|
${auditSelect('i', 'item_created_user', 'item_updated_user')},
|
||||||
${uploadedImageJson('i.image_path')} AS image,
|
${uploadedImageJson('i.image_path')} AS image,
|
||||||
@@ -3873,7 +3936,13 @@ export async function getItem(id: number, locale = defaultLocale) {
|
|||||||
query(
|
query(
|
||||||
`
|
`
|
||||||
SELECT
|
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
|
json_build_object('id', s.id, 'name', ${skillName}) AS skill
|
||||||
FROM pokemon_skill_item_drops psid
|
FROM pokemon_skill_item_drops psid
|
||||||
JOIN pokemon p ON p.id = psid.pokemon_id
|
JOIN pokemon p ON p.id = psid.pokemon_id
|
||||||
@@ -3905,6 +3974,7 @@ function cleanItemPayload(payload: Record<string, unknown>): ItemPayload {
|
|||||||
dualDyeable: Boolean(payload.dualDyeable),
|
dualDyeable: Boolean(payload.dualDyeable),
|
||||||
patternEditable: Boolean(payload.patternEditable),
|
patternEditable: Boolean(payload.patternEditable),
|
||||||
noRecipe: Boolean(payload.noRecipe),
|
noRecipe: Boolean(payload.noRecipe),
|
||||||
|
isEventItem: Boolean(payload.isEventItem),
|
||||||
acquisitionMethodIds: cleanIds(payload.acquisitionMethodIds),
|
acquisitionMethodIds: cleanIds(payload.acquisitionMethodIds),
|
||||||
tagIds: cleanIds(payload.tagIds),
|
tagIds: cleanIds(payload.tagIds),
|
||||||
imagePath: cleanUploadImagePath(payload.imagePath, 'items')
|
imagePath: cleanUploadImagePath(payload.imagePath, 'items')
|
||||||
@@ -3956,12 +4026,13 @@ export async function createItem(payload: Record<string, unknown>, userId: numbe
|
|||||||
dual_dyeable,
|
dual_dyeable,
|
||||||
pattern_editable,
|
pattern_editable,
|
||||||
no_recipe,
|
no_recipe,
|
||||||
|
is_event_item,
|
||||||
image_path,
|
image_path,
|
||||||
sort_order,
|
sort_order,
|
||||||
created_by_user_id,
|
created_by_user_id,
|
||||||
updated_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
|
RETURNING id
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
@@ -3972,6 +4043,7 @@ export async function createItem(payload: Record<string, unknown>, userId: numbe
|
|||||||
cleanPayload.dualDyeable,
|
cleanPayload.dualDyeable,
|
||||||
cleanPayload.patternEditable,
|
cleanPayload.patternEditable,
|
||||||
cleanPayload.noRecipe,
|
cleanPayload.noRecipe,
|
||||||
|
cleanPayload.isEventItem,
|
||||||
cleanPayload.imagePath,
|
cleanPayload.imagePath,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
userId
|
userId
|
||||||
@@ -4003,10 +4075,11 @@ export async function updateItem(id: number, payload: Record<string, unknown>, u
|
|||||||
dual_dyeable = $5,
|
dual_dyeable = $5,
|
||||||
pattern_editable = $6,
|
pattern_editable = $6,
|
||||||
no_recipe = $7,
|
no_recipe = $7,
|
||||||
image_path = $8,
|
is_event_item = $8,
|
||||||
updated_by_user_id = $9,
|
image_path = $9,
|
||||||
|
updated_by_user_id = $10,
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
WHERE id = $10
|
WHERE id = $11
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
cleanPayload.name,
|
cleanPayload.name,
|
||||||
@@ -4016,6 +4089,7 @@ export async function updateItem(id: number, payload: Record<string, unknown>, u
|
|||||||
cleanPayload.dualDyeable,
|
cleanPayload.dualDyeable,
|
||||||
cleanPayload.patternEditable,
|
cleanPayload.patternEditable,
|
||||||
cleanPayload.noRecipe,
|
cleanPayload.noRecipe,
|
||||||
|
cleanPayload.isEventItem,
|
||||||
cleanPayload.imagePath,
|
cleanPayload.imagePath,
|
||||||
userId,
|
userId,
|
||||||
id
|
id
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ const changeLabelKeys: Record<string, string> = {
|
|||||||
Name: 'common.name',
|
Name: 'common.name',
|
||||||
名字: 'common.name',
|
名字: 'common.name',
|
||||||
名称: 'common.name',
|
名称: 'common.name',
|
||||||
|
'Pokemon ID': 'pages.pokemon.id',
|
||||||
|
'Event item': 'common.eventItem',
|
||||||
Genus: 'pages.pokemon.genus',
|
Genus: 'pages.pokemon.genus',
|
||||||
Details: 'pages.pokemon.details',
|
Details: 'pages.pokemon.details',
|
||||||
介绍: 'pages.pokemon.details',
|
介绍: 'pages.pokemon.details',
|
||||||
|
|||||||
@@ -106,8 +106,10 @@ export interface EditHistoryEntry {
|
|||||||
|
|
||||||
export interface Pokemon extends EditInfo {
|
export interface Pokemon extends EditInfo {
|
||||||
id: number;
|
id: number;
|
||||||
|
displayId: number;
|
||||||
name: string;
|
name: string;
|
||||||
baseName?: string;
|
baseName?: string;
|
||||||
|
isEventItem: boolean;
|
||||||
genus: string;
|
genus: string;
|
||||||
baseGenus?: string;
|
baseGenus?: string;
|
||||||
details: string;
|
details: string;
|
||||||
@@ -127,7 +129,9 @@ export interface Pokemon extends EditInfo {
|
|||||||
|
|
||||||
export interface RelatedPokemon {
|
export interface RelatedPokemon {
|
||||||
id: number;
|
id: number;
|
||||||
|
displayId: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
isEventItem: boolean;
|
||||||
image?: PokemonImage | null;
|
image?: PokemonImage | null;
|
||||||
environment: NamedEntity;
|
environment: NamedEntity;
|
||||||
skills: Skill[];
|
skills: Skill[];
|
||||||
@@ -155,6 +159,7 @@ export interface Habitat extends EditInfo {
|
|||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
baseName?: string;
|
baseName?: string;
|
||||||
|
isEventItem: boolean;
|
||||||
translations?: TranslationMap;
|
translations?: TranslationMap;
|
||||||
image: EntityImage | null;
|
image: EntityImage | null;
|
||||||
recipe: Array<NamedEntity & { image?: EntityImage | null; quantity: number }>;
|
recipe: Array<NamedEntity & { image?: EntityImage | null; quantity: number }>;
|
||||||
@@ -165,6 +170,8 @@ export interface HabitatDetail extends Habitat {
|
|||||||
editHistory: EditHistoryEntry[];
|
editHistory: EditHistoryEntry[];
|
||||||
imageHistory: EntityImageUpload[];
|
imageHistory: EntityImageUpload[];
|
||||||
pokemon: Array<NamedEntity & {
|
pokemon: Array<NamedEntity & {
|
||||||
|
displayId: number;
|
||||||
|
isEventItem: boolean;
|
||||||
image?: PokemonImage | null;
|
image?: PokemonImage | null;
|
||||||
time_of_day: string;
|
time_of_day: string;
|
||||||
weather: string;
|
weather: string;
|
||||||
@@ -201,6 +208,7 @@ export interface Item extends EditInfo {
|
|||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
baseName?: string;
|
baseName?: string;
|
||||||
|
isEventItem: boolean;
|
||||||
translations?: TranslationMap;
|
translations?: TranslationMap;
|
||||||
image: EntityImage | null;
|
image: EntityImage | null;
|
||||||
category: NamedEntity;
|
category: NamedEntity;
|
||||||
@@ -223,7 +231,7 @@ export interface ItemDetail extends Item {
|
|||||||
editHistory: EditHistoryEntry[];
|
editHistory: EditHistoryEntry[];
|
||||||
imageHistory: EntityImageUpload[];
|
imageHistory: EntityImageUpload[];
|
||||||
droppedByPokemon: Array<{
|
droppedByPokemon: Array<{
|
||||||
pokemon: NamedEntity & { image?: PokemonImage | null };
|
pokemon: NamedEntity & { displayId: number; isEventItem: boolean; image?: PokemonImage | null };
|
||||||
skill: NamedEntity;
|
skill: NamedEntity;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
@@ -339,7 +347,8 @@ export type ConfigType =
|
|||||||
| 'life-tags';
|
| 'life-tags';
|
||||||
|
|
||||||
export interface PokemonPayload {
|
export interface PokemonPayload {
|
||||||
id: number;
|
displayId: number;
|
||||||
|
isEventItem: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
genus: string;
|
genus: string;
|
||||||
details: string;
|
details: string;
|
||||||
@@ -388,6 +397,7 @@ export interface ItemPayload {
|
|||||||
dualDyeable: boolean;
|
dualDyeable: boolean;
|
||||||
patternEditable: boolean;
|
patternEditable: boolean;
|
||||||
noRecipe: boolean;
|
noRecipe: boolean;
|
||||||
|
isEventItem: boolean;
|
||||||
acquisitionMethodIds: number[];
|
acquisitionMethodIds: number[];
|
||||||
tagIds: number[];
|
tagIds: number[];
|
||||||
imagePath: string;
|
imagePath: string;
|
||||||
@@ -402,6 +412,7 @@ export interface RecipePayload {
|
|||||||
export interface HabitatPayload {
|
export interface HabitatPayload {
|
||||||
name: string;
|
name: string;
|
||||||
translations?: TranslationMap;
|
translations?: TranslationMap;
|
||||||
|
isEventItem: boolean;
|
||||||
imagePath: string;
|
imagePath: string;
|
||||||
recipeItems: Array<{ itemId: number; quantity: number }>;
|
recipeItems: Array<{ itemId: number; quantity: number }>;
|
||||||
pokemonAppearances: Array<{
|
pokemonAppearances: Array<{
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ const languageLabel = (item: Language) => item.name;
|
|||||||
const configKey = (item: EditableConfig) => item.id;
|
const configKey = (item: EditableConfig) => item.id;
|
||||||
const configLabel = (item: EditableConfig) => item.name;
|
const configLabel = (item: EditableConfig) => item.name;
|
||||||
const pokemonKey = (item: Pokemon) => item.id;
|
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 itemKey = (item: Item) => item.id;
|
||||||
const itemLabel = (item: Item) => item.name;
|
const itemLabel = (item: Item) => item.name;
|
||||||
const recipeKey = (item: Recipe) => item.id;
|
const recipeKey = (item: Recipe) => item.id;
|
||||||
@@ -921,7 +921,7 @@ onMounted(() => {
|
|||||||
@reorder="persistPokemonOrder"
|
@reorder="persistPokemonOrder"
|
||||||
>
|
>
|
||||||
<template #default="{ item }">
|
<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">
|
<span class="row-actions">
|
||||||
<button type="button" :disabled="busy" @click="removePokemon(item.id)">
|
<button type="button" :disabled="busy" @click="removePokemon(item.id)">
|
||||||
<Icon :icon="iconDelete" class="ui-icon" aria-hidden="true" />
|
<Icon :icon="iconDelete" class="ui-icon" aria-hidden="true" />
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ const creatingSelect = ref('');
|
|||||||
const habitatForm = ref({
|
const habitatForm = ref({
|
||||||
name: '',
|
name: '',
|
||||||
translations: {} as TranslationMap,
|
translations: {} as TranslationMap,
|
||||||
|
isEventItem: false,
|
||||||
imagePath: '',
|
imagePath: '',
|
||||||
recipeItems: [] as Array<{ itemId: string; quantity: number }>,
|
recipeItems: [] as Array<{ itemId: string; quantity: number }>,
|
||||||
pokemonAppearances: [] as HabitatAppearanceForm[]
|
pokemonAppearances: [] as HabitatAppearanceForm[]
|
||||||
@@ -71,7 +72,7 @@ const routeId = computed(() => (typeof route.params.id === 'string' ? route.para
|
|||||||
const isEditing = computed(() => routeId.value !== '');
|
const isEditing = computed(() => routeId.value !== '');
|
||||||
const itemSelectOptions = computed(() => itemRows.value.map((item) => ({ id: item.id, name: item.name })));
|
const itemSelectOptions = computed(() => itemRows.value.map((item) => ({ id: item.id, name: item.name })));
|
||||||
const pokemonSelectOptions = computed(() =>
|
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(() =>
|
const pageTitle = computed(() =>
|
||||||
isEditing.value
|
isEditing.value
|
||||||
@@ -166,6 +167,7 @@ async function loadEditor() {
|
|||||||
habitatForm.value = {
|
habitatForm.value = {
|
||||||
name: habitat.baseName ?? habitat.name,
|
name: habitat.baseName ?? habitat.name,
|
||||||
translations: habitat.translations ?? {},
|
translations: habitat.translations ?? {},
|
||||||
|
isEventItem: habitat.isEventItem,
|
||||||
imagePath: habitat.image?.path ?? '',
|
imagePath: habitat.image?.path ?? '',
|
||||||
recipeItems: habitat.recipe.map((recipeItem) => ({ itemId: String(recipeItem.id), quantity: recipeItem.quantity })),
|
recipeItems: habitat.recipe.map((recipeItem) => ({ itemId: String(recipeItem.id), quantity: recipeItem.quantity })),
|
||||||
pokemonAppearances: groupPokemonAppearances(habitat)
|
pokemonAppearances: groupPokemonAppearances(habitat)
|
||||||
@@ -212,6 +214,7 @@ async function saveHabitat() {
|
|||||||
const payload: HabitatPayload = {
|
const payload: HabitatPayload = {
|
||||||
name: habitatNameForSave(),
|
name: habitatNameForSave(),
|
||||||
translations: habitatForm.value.translations,
|
translations: habitatForm.value.translations,
|
||||||
|
isEventItem: habitatForm.value.isEventItem,
|
||||||
imagePath: habitatForm.value.imagePath,
|
imagePath: habitatForm.value.imagePath,
|
||||||
recipeItems: toQuantityRows(habitatForm.value.recipeItems),
|
recipeItems: toQuantityRows(habitatForm.value.recipeItems),
|
||||||
pokemonAppearances: habitatForm.value.pokemonAppearances
|
pokemonAppearances: habitatForm.value.pokemonAppearances
|
||||||
@@ -276,6 +279,10 @@ onMounted(() => {
|
|||||||
@error="message = $event"
|
@error="message = $event"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div class="check-row">
|
||||||
|
<label><input v-model="habitatForm.isEventItem" type="checkbox" /> {{ t('pages.habitats.eventItem') }}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>{{ t('pages.habitats.recipe') }}</label>
|
<label>{{ t('pages.habitats.recipe') }}</label>
|
||||||
<div v-for="(row, index) in habitatForm.recipeItems" :key="index" class="inline-row">
|
<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" />
|
<img v-if="entry.pokemon.image" :src="entry.pokemon.image.url" alt="" loading="lazy" />
|
||||||
<PokeBallMark v-else size="22px" />
|
<PokeBallMark v-else size="22px" />
|
||||||
</span>
|
</span>
|
||||||
<span>#{{ entry.pokemon.id }} {{ entry.pokemon.name }}</span>
|
<span>#{{ entry.pokemon.displayId }} {{ entry.pokemon.name }}</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<span>{{ t('pages.pokemon.skillDrop', { name: entry.skill.name }) }}</span>
|
<span>{{ t('pages.pokemon.skillDrop', { name: entry.skill.name }) }}</span>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const itemForm = ref({
|
|||||||
dualDyeable: false,
|
dualDyeable: false,
|
||||||
patternEditable: false,
|
patternEditable: false,
|
||||||
noRecipe: false,
|
noRecipe: false,
|
||||||
|
isEventItem: false,
|
||||||
acquisitionMethodIds: [] as string[],
|
acquisitionMethodIds: [] as string[],
|
||||||
tagIds: [] as string[],
|
tagIds: [] as string[],
|
||||||
imagePath: ''
|
imagePath: ''
|
||||||
@@ -92,6 +93,7 @@ async function loadEditor() {
|
|||||||
dualDyeable: item.customization.dualDyeable,
|
dualDyeable: item.customization.dualDyeable,
|
||||||
patternEditable: item.customization.patternEditable,
|
patternEditable: item.customization.patternEditable,
|
||||||
noRecipe: item.noRecipe,
|
noRecipe: item.noRecipe,
|
||||||
|
isEventItem: item.isEventItem,
|
||||||
acquisitionMethodIds: item.acquisitionMethods.map((method) => String(method.id)),
|
acquisitionMethodIds: item.acquisitionMethods.map((method) => String(method.id)),
|
||||||
tagIds: item.tags.map((tag) => String(tag.id)),
|
tagIds: item.tags.map((tag) => String(tag.id)),
|
||||||
imagePath: item.image?.path ?? ''
|
imagePath: item.image?.path ?? ''
|
||||||
@@ -158,6 +160,7 @@ async function saveItem() {
|
|||||||
dualDyeable: itemForm.value.dualDyeable,
|
dualDyeable: itemForm.value.dualDyeable,
|
||||||
patternEditable: itemForm.value.patternEditable,
|
patternEditable: itemForm.value.patternEditable,
|
||||||
noRecipe: itemForm.value.noRecipe,
|
noRecipe: itemForm.value.noRecipe,
|
||||||
|
isEventItem: itemForm.value.isEventItem,
|
||||||
acquisitionMethodIds: toIds(itemForm.value.acquisitionMethodIds),
|
acquisitionMethodIds: toIds(itemForm.value.acquisitionMethodIds),
|
||||||
tagIds: toIds(itemForm.value.tagIds),
|
tagIds: toIds(itemForm.value.tagIds),
|
||||||
imagePath: itemForm.value.imagePath
|
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.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.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.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>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
|||||||
@@ -304,7 +304,7 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section v-else class="page-stack">
|
<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 #kicker>Pokédex Detail</template>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<RouterLink class="ui-button ui-button--primary ui-button--small" :to="`/pokemon/${pokemon.id}/edit`">
|
<RouterLink class="ui-button ui-button--primary ui-button--small" :to="`/pokemon/${pokemon.id}/edit`">
|
||||||
@@ -421,7 +421,7 @@ watch(
|
|||||||
</span>
|
</span>
|
||||||
<div class="related-pokemon-row">
|
<div class="related-pokemon-row">
|
||||||
<div class="related-pokemon-row__summary">
|
<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">
|
<div class="related-pokemon-row__traits">
|
||||||
<EntityChips
|
<EntityChips
|
||||||
v-if="related.skills.length"
|
v-if="related.skills.length"
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ function defaultPokemonStats(): PokemonStats {
|
|||||||
|
|
||||||
const pokemonForm = ref({
|
const pokemonForm = ref({
|
||||||
id: '',
|
id: '',
|
||||||
|
isEventItem: false,
|
||||||
name: '',
|
name: '',
|
||||||
genus: '',
|
genus: '',
|
||||||
details: '',
|
details: '',
|
||||||
@@ -206,7 +207,7 @@ function pokemonNameForSave() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function pokemonIdForSave() {
|
function pokemonIdForSave() {
|
||||||
return Number(isEditing.value ? routeId.value : pokemonForm.value.id);
|
return Number(pokemonForm.value.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeFetchedTranslations(fetchedTranslations: TranslationMap | undefined): TranslationMap {
|
function mergeFetchedTranslations(fetchedTranslations: TranslationMap | undefined): TranslationMap {
|
||||||
@@ -273,7 +274,8 @@ async function loadEditor() {
|
|||||||
if (isEditing.value) {
|
if (isEditing.value) {
|
||||||
const pokemon = await api.pokemonDetail(routeId.value);
|
const pokemon = await api.pokemonDetail(routeId.value);
|
||||||
pokemonForm.value = {
|
pokemonForm.value = {
|
||||||
id: String(pokemon.id),
|
id: String(pokemon.displayId),
|
||||||
|
isEventItem: pokemon.isEventItem,
|
||||||
name: pokemon.baseName ?? pokemon.name,
|
name: pokemon.baseName ?? pokemon.name,
|
||||||
genus: pokemon.baseGenus ?? pokemon.genus,
|
genus: pokemon.baseGenus ?? pokemon.genus,
|
||||||
details: pokemon.baseDetails ?? pokemon.details,
|
details: pokemon.baseDetails ?? pokemon.details,
|
||||||
@@ -535,7 +537,8 @@ async function savePokemon() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const payload: PokemonPayload = {
|
const payload: PokemonPayload = {
|
||||||
id: pokemonIdForSave(),
|
displayId: pokemonIdForSave(),
|
||||||
|
isEventItem: pokemonForm.value.isEventItem,
|
||||||
name: pokemonNameForSave(),
|
name: pokemonNameForSave(),
|
||||||
genus: pokemonForm.value.genus,
|
genus: pokemonForm.value.genus,
|
||||||
details: pokemonForm.value.details,
|
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')">
|
<section v-if="activeEditTab === 'basic'" class="pokemon-edit-panel" role="tabpanel" :aria-label="t('pages.pokemon.editTabBasic')">
|
||||||
<div class="pokemon-edit-grid">
|
<div class="pokemon-edit-grid">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="pokemon-id">ID</label>
|
<label for="pokemon-id">{{ t('pages.pokemon.id') }}</label>
|
||||||
<input id="pokemon-id" v-model="pokemonForm.id" :disabled="isEditing" min="1" required type="number" />
|
<input id="pokemon-id" v-model="pokemonForm.id" min="1" required type="number" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TranslationFields
|
<TranslationFields
|
||||||
@@ -645,6 +648,10 @@ watch(fetchIdentifier, refreshFetchOptions);
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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="pokemon-edit-grid">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="pokemon-environment">{{ t('pages.pokemon.environment') }}</label>
|
<label for="pokemon-environment">{{ t('pages.pokemon.environment') }}</label>
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ watch(query, loadPokemon);
|
|||||||
<EntityCard
|
<EntityCard
|
||||||
v-for="item in pokemon"
|
v-for="item in pokemon"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:title="`#${item.id} ${item.name}`"
|
:title="`#${item.displayId} ${item.name}`"
|
||||||
:to="`/pokemon/${item.id}`"
|
:to="`/pokemon/${item.id}`"
|
||||||
:image="pokemonCardImage(item)"
|
:image="pokemonCardImage(item)"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export const systemWordingMessages = {
|
|||||||
inDev: 'In-Dev',
|
inDev: 'In-Dev',
|
||||||
removeNamed: 'Remove {name}',
|
removeNamed: 'Remove {name}',
|
||||||
quantity: 'Quantity',
|
quantity: 'Quantity',
|
||||||
|
eventItem: 'Event item',
|
||||||
required: 'Required'
|
required: 'Required'
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
@@ -133,6 +134,7 @@ export const systemWordingMessages = {
|
|||||||
editTabAdvance: 'Advance',
|
editTabAdvance: 'Advance',
|
||||||
newTitle: 'New Pokemon',
|
newTitle: 'New Pokemon',
|
||||||
editTitle: 'Edit #{id} {name}',
|
editTitle: 'Edit #{id} {name}',
|
||||||
|
id: 'Pokemon ID',
|
||||||
fetchData: 'Fetch data',
|
fetchData: 'Fetch data',
|
||||||
fetchingData: 'Fetching',
|
fetchingData: 'Fetching',
|
||||||
fetchIdentifier: 'Data identifier',
|
fetchIdentifier: 'Data identifier',
|
||||||
@@ -155,6 +157,7 @@ export const systemWordingMessages = {
|
|||||||
clearImage: 'Clear image',
|
clearImage: 'Clear image',
|
||||||
imageEmpty: 'No Pokemon image selected',
|
imageEmpty: 'No Pokemon image selected',
|
||||||
imageAlt: '{name} {variant} image',
|
imageAlt: '{name} {variant} image',
|
||||||
|
eventItem: 'Event item',
|
||||||
loadingList: 'Loading Pokemon list',
|
loadingList: 'Loading Pokemon list',
|
||||||
loadingDetail: 'Loading Pokemon detail',
|
loadingDetail: 'Loading Pokemon detail',
|
||||||
loadingEdit: 'Loading Pokemon editor',
|
loadingEdit: 'Loading Pokemon editor',
|
||||||
@@ -221,6 +224,7 @@ export const systemWordingMessages = {
|
|||||||
loadingList: 'Loading habitat list',
|
loadingList: 'Loading habitat list',
|
||||||
loadingDetail: 'Loading habitat detail',
|
loadingDetail: 'Loading habitat detail',
|
||||||
loadingEdit: 'Loading habitat editor',
|
loadingEdit: 'Loading habitat editor',
|
||||||
|
eventItem: 'Event item',
|
||||||
recipe: 'Recipe',
|
recipe: 'Recipe',
|
||||||
recipeList: 'Recipe list',
|
recipeList: 'Recipe list',
|
||||||
possiblePokemon: 'Possible Pokemon',
|
possiblePokemon: 'Possible Pokemon',
|
||||||
@@ -251,6 +255,7 @@ export const systemWordingMessages = {
|
|||||||
dualDyeable: 'Dual dyeable',
|
dualDyeable: 'Dual dyeable',
|
||||||
patternEditable: 'Pattern editable',
|
patternEditable: 'Pattern editable',
|
||||||
noRecipe: 'No recipe',
|
noRecipe: 'No recipe',
|
||||||
|
eventItem: 'Event item',
|
||||||
recipeInfo: 'Recipe info',
|
recipeInfo: 'Recipe info',
|
||||||
relatedRecipes: 'Related recipes',
|
relatedRecipes: 'Related recipes',
|
||||||
relatedHabitats: 'Related habitats',
|
relatedHabitats: 'Related habitats',
|
||||||
@@ -686,6 +691,7 @@ export const systemWordingMessages = {
|
|||||||
inDev: '开发中',
|
inDev: '开发中',
|
||||||
removeNamed: '移除{name}',
|
removeNamed: '移除{name}',
|
||||||
quantity: '数量',
|
quantity: '数量',
|
||||||
|
eventItem: '活动物品',
|
||||||
required: '必填'
|
required: '必填'
|
||||||
},
|
},
|
||||||
nav: {
|
nav: {
|
||||||
@@ -781,6 +787,7 @@ export const systemWordingMessages = {
|
|||||||
editTabAdvance: '进阶',
|
editTabAdvance: '进阶',
|
||||||
newTitle: '新增 Pokemon',
|
newTitle: '新增 Pokemon',
|
||||||
editTitle: '编辑 #{id} {name}',
|
editTitle: '编辑 #{id} {name}',
|
||||||
|
id: 'Pokemon ID',
|
||||||
fetchData: '获取数据',
|
fetchData: '获取数据',
|
||||||
fetchingData: '正在获取',
|
fetchingData: '正在获取',
|
||||||
fetchIdentifier: '数据标识',
|
fetchIdentifier: '数据标识',
|
||||||
@@ -803,6 +810,7 @@ export const systemWordingMessages = {
|
|||||||
clearImage: '清除图片',
|
clearImage: '清除图片',
|
||||||
imageEmpty: '尚未选择 Pokemon 图片',
|
imageEmpty: '尚未选择 Pokemon 图片',
|
||||||
imageAlt: '{name} {variant} 图片',
|
imageAlt: '{name} {variant} 图片',
|
||||||
|
eventItem: '活动物品',
|
||||||
loadingList: '正在加载 Pokemon 列表',
|
loadingList: '正在加载 Pokemon 列表',
|
||||||
loadingDetail: '正在加载 Pokemon 详情',
|
loadingDetail: '正在加载 Pokemon 详情',
|
||||||
loadingEdit: '正在加载 Pokemon 编辑内容',
|
loadingEdit: '正在加载 Pokemon 编辑内容',
|
||||||
@@ -869,6 +877,7 @@ export const systemWordingMessages = {
|
|||||||
loadingList: '正在加载栖息地列表',
|
loadingList: '正在加载栖息地列表',
|
||||||
loadingDetail: '正在加载栖息地详情',
|
loadingDetail: '正在加载栖息地详情',
|
||||||
loadingEdit: '正在加载栖息地编辑内容',
|
loadingEdit: '正在加载栖息地编辑内容',
|
||||||
|
eventItem: '活动物品',
|
||||||
recipe: '配方',
|
recipe: '配方',
|
||||||
recipeList: '配方列表',
|
recipeList: '配方列表',
|
||||||
possiblePokemon: '可能出现的宝可梦',
|
possiblePokemon: '可能出现的宝可梦',
|
||||||
@@ -899,6 +908,7 @@ export const systemWordingMessages = {
|
|||||||
dualDyeable: '可双区染色',
|
dualDyeable: '可双区染色',
|
||||||
patternEditable: '可改花纹',
|
patternEditable: '可改花纹',
|
||||||
noRecipe: '无材料单',
|
noRecipe: '无材料单',
|
||||||
|
eventItem: '活动物品',
|
||||||
recipeInfo: '材料单信息',
|
recipeInfo: '材料单信息',
|
||||||
relatedRecipes: '相关材料单',
|
relatedRecipes: '相关材料单',
|
||||||
relatedHabitats: '相关栖息地',
|
relatedHabitats: '相关栖息地',
|
||||||
|
|||||||
Reference in New Issue
Block a user