feat(items): add base price and support usage in creation defaults

Add `base_price` to items schema, API, and edit history
Display and edit base price in item details and forms
Add `clearable` prop to TagsSelect for optional single selections
Include usage in item creation session defaults
This commit is contained in:
2026-05-05 08:59:36 +08:00
parent 8ee29e9549
commit 9312156a3c
10 changed files with 118 additions and 20 deletions

View File

@@ -596,6 +596,7 @@ Pokemon 详情页展示:
- 名称 - 名称
- 介绍 - 介绍
- Base Price可为空
- 是否为 Event Item`is_event_item` - 是否为 Event Item`is_event_item`
- 分类:必填,使用系统固定列表,不在管理端配置: - 分类:必填,使用系统固定列表,不在管理端配置:
- Furniture - Furniture
@@ -640,7 +641,7 @@ Items 与 Event Items 使用相同数据模型:
- 按标签筛选 - 按标签筛选
- 按自定义排序展示 - 按自定义排序展示
- All 视图在满足写入权限时支持对 Grid Item 右键插入新物品到前/后,并支持直接拖曳 Item 调整排序;插入与拖曳只作用于当前展示的 Items 列表,不影响 Event Items 入口。 - All 视图在满足写入权限时支持对 Grid Item 右键插入新物品到前/后,并支持直接拖曳 Item 调整排序;插入与拖曳只作用于当前展示的 Items 列表,不影响 Event Items 入口。
- 新增物品入口支持当前浏览器 Session 的默认值菜单;用户可为新建物品预设分类、客制化勾选项和入手方式。默认值只影响 `/items/new``/event-items/new` 的新建表单初始值,不影响编辑已有物品,不改变 API、数据库模型、权限或审计行为Event Items 仍由 `/event-items/new` 入口决定 `is_event_item` - 新增物品入口支持当前浏览器 Session 的默认值菜单;用户可为新建物品预设分类、用途、客制化勾选项和入手方式。默认值只影响 `/items/new``/event-items/new` 的新建表单初始值,不影响编辑已有物品,不改变 API、数据库模型、权限或审计行为Event Items 仍由 `/event-items/new` 入口决定 `is_event_item`
- 物品列表桌面端使用 12 列紧凑 Grid每个格子只展示物品图标有用途的物品在卡片左上角以斜 Ribbon 展示用途名称;物品名称通过 hover / focus Tooltip 展示。 - 物品列表桌面端使用 12 列紧凑 Grid每个格子只展示物品图标有用途的物品在卡片左上角以斜 Ribbon 展示用途名称;物品名称通过 hover / focus Tooltip 展示。
- 物品列表移动端保持常规卡片布局,展示物品图标、名称和分类。 - 物品列表移动端保持常规卡片布局,展示物品图标、名称和分类。
- 物品列表不展示标签、入手方式或编辑元信息。 - 物品列表不展示标签、入手方式或编辑元信息。
@@ -652,6 +653,7 @@ Items 与 Event Items 使用相同数据模型:
- 当前图标图片;未配置图标时展示默认物品标记占位符 - 当前图标图片;未配置图标时展示默认物品标记占位符
- 顶部按图标 / 占位符与核心信息概览并排展示,移动端改为单列;顶部概览卡片不显示 `Image` / `Details` 通用区块标题,也不展示图片历史缩略图 - 顶部按图标 / 占位符与核心信息概览并排展示,移动端改为单列;顶部概览卡片不显示 `Image` / `Details` 通用区块标题,也不展示图片历史缩略图
- 介绍 - 介绍
- Base Price
- 分类 - 分类
- 用途 - 用途
- 入手方式 - 入手方式

View File

@@ -976,6 +976,7 @@ CREATE TABLE IF NOT EXISTS items (
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,
details text NOT NULL DEFAULT '', details text NOT NULL DEFAULT '',
base_price integer,
category_key text NOT NULL DEFAULT 'other', category_key text NOT NULL DEFAULT 'other',
usage_key text, usage_key text,
category_id integer REFERENCES item_categories(id), category_id integer REFERENCES item_categories(id),
@@ -1220,9 +1221,14 @@ ALTER TABLE life_tags
ALTER TABLE items ALTER TABLE items
ADD COLUMN IF NOT EXISTS details text NOT NULL DEFAULT '', ADD COLUMN IF NOT EXISTS details text NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS base_price integer,
ADD COLUMN IF NOT EXISTS category_key text, ADD COLUMN IF NOT EXISTS category_key text,
ADD COLUMN IF NOT EXISTS usage_key text; ADD COLUMN IF NOT EXISTS usage_key text;
UPDATE items
SET base_price = NULL
WHERE base_price < 0;
ALTER TABLE ancient_artifacts ALTER TABLE ancient_artifacts
ADD COLUMN IF NOT EXISTS image_path text NOT NULL DEFAULT ''; ADD COLUMN IF NOT EXISTS image_path text NOT NULL DEFAULT '';
@@ -1301,10 +1307,12 @@ ALTER TABLE items
ALTER TABLE items ALTER TABLE items
DROP CONSTRAINT IF EXISTS items_display_id_positive, DROP CONSTRAINT IF EXISTS items_display_id_positive,
DROP CONSTRAINT IF EXISTS items_base_price_check,
DROP CONSTRAINT IF EXISTS items_category_key_check, DROP CONSTRAINT IF EXISTS items_category_key_check,
DROP CONSTRAINT IF EXISTS items_usage_key_check; DROP CONSTRAINT IF EXISTS items_usage_key_check;
ALTER TABLE items ALTER TABLE items
ADD CONSTRAINT items_base_price_check CHECK (base_price IS NULL OR base_price >= 0),
ADD CONSTRAINT items_category_key_check CHECK (category_key IN ( ADD CONSTRAINT items_category_key_check CHECK (category_key IN (
'furniture', 'furniture',
'misc', 'misc',

View File

@@ -203,6 +203,7 @@ type PokemonCsvData = {
type ItemPayload = { type ItemPayload = {
name: string; name: string;
details: string; details: string;
basePrice: number | null;
translations: TranslationInput; translations: TranslationInput;
categoryId: number; categoryId: number;
categoryKey: string; categoryKey: string;
@@ -543,6 +544,7 @@ type PokemonChangeSource = {
type ItemChangeSource = { type ItemChangeSource = {
name: string; name: string;
details: string; details: string;
basePrice: number | null;
isEventItem: boolean; isEventItem: boolean;
image: EntityImageValue | null; image: EntityImageValue | null;
category: { name: string }; category: { name: string };
@@ -1077,6 +1079,20 @@ function cleanNonNegativeNumber(value: unknown, message: string): number {
return numberValue; return numberValue;
} }
function cleanOptionalNonNegativeInteger(value: unknown, message: string): number | null {
const rawValue = typeof value === 'string' ? value.trim() : value;
if (rawValue === undefined || rawValue === null || rawValue === '') {
return null;
}
const numberValue = Number(rawValue);
if (!Number.isInteger(numberValue) || numberValue < 0) {
throw validationError(message);
}
return numberValue;
}
function cleanQuantities(value: unknown): IdQuantity[] { function cleanQuantities(value: unknown): IdQuantity[] {
if (!Array.isArray(value)) { if (!Array.isArray(value)) {
return []; return [];
@@ -2246,6 +2262,12 @@ async function itemEditChanges(
pushChange(changes, 'Name', before.name, after.name); pushChange(changes, 'Name', before.name, after.name);
pushChange(changes, 'Description', before.details, after.details); pushChange(changes, 'Description', before.details, after.details);
pushChange(
changes,
'Base Price',
before.basePrice === null ? null : String(before.basePrice),
after.basePrice === null ? null : String(after.basePrice)
);
pushTranslationChanges(changes, before.translations, after.translations, ['name', 'details']); pushTranslationChanges(changes, before.translations, after.translations, ['name', 'details']);
pushChange(changes, 'Event item', boolValue(before.isEventItem), boolValue(after.isEventItem)); 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));
@@ -6216,6 +6238,7 @@ function itemProjection(locale: string): string {
i.name AS "baseName", i.name AS "baseName",
${itemDetails} AS details, ${itemDetails} AS details,
i.details AS "baseDetails", i.details AS "baseDetails",
i.base_price AS "basePrice",
i.is_event_item AS "isEventItem", 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')},
@@ -6484,6 +6507,7 @@ function cleanItemPayload(payload: Record<string, unknown>): ItemPayload {
return { return {
name: cleanName(payload.name, 'server.validation.itemNameRequired'), name: cleanName(payload.name, 'server.validation.itemNameRequired'),
details: cleanOptionalText(payload.details), details: cleanOptionalText(payload.details),
basePrice: cleanOptionalNonNegativeInteger(payload.basePrice, 'server.validation.invalidField'),
translations: cleanTranslations(payload.translations, ['name', 'details']), translations: cleanTranslations(payload.translations, ['name', 'details']),
categoryId, categoryId,
categoryKey: category.key, categoryKey: category.key,
@@ -6558,6 +6582,7 @@ export async function createItem(payload: Record<string, unknown>, userId: numbe
INSERT INTO items ( INSERT INTO items (
name, name,
details, details,
base_price,
category_key, category_key,
usage_key, usage_key,
dyeable, dyeable,
@@ -6570,12 +6595,13 @@ export async function createItem(payload: Record<string, unknown>, userId: numbe
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, $12) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $13)
RETURNING id RETURNING id
`, `,
[ [
cleanPayload.name, cleanPayload.name,
cleanPayload.details, cleanPayload.details,
cleanPayload.basePrice,
cleanPayload.categoryKey, cleanPayload.categoryKey,
cleanPayload.usageKey, cleanPayload.usageKey,
cleanPayload.dyeable, cleanPayload.dyeable,
@@ -6631,21 +6657,23 @@ export async function updateItem(id: number, payload: Record<string, unknown>, u
UPDATE items UPDATE items
SET name = $1, SET name = $1,
details = $2, details = $2,
category_key = $3, base_price = $3,
usage_key = $4, category_key = $4,
dyeable = $5, usage_key = $5,
dual_dyeable = $6, dyeable = $6,
pattern_editable = $7, dual_dyeable = $7,
no_recipe = $8, pattern_editable = $8,
is_event_item = $9, no_recipe = $9,
image_path = $10, is_event_item = $10,
updated_by_user_id = $11, image_path = $11,
updated_by_user_id = $12,
updated_at = now() updated_at = now()
WHERE id = $12 WHERE id = $13
`, `,
[ [
cleanPayload.name, cleanPayload.name,
cleanPayload.details, cleanPayload.details,
cleanPayload.basePrice,
cleanPayload.categoryKey, cleanPayload.categoryKey,
cleanPayload.usageKey, cleanPayload.usageKey,
cleanPayload.dyeable, cleanPayload.dyeable,
@@ -7586,6 +7614,7 @@ const dataToolColumns = {
'id', 'id',
'name', 'name',
'details', 'details',
'base_price',
'category_key', 'category_key',
'usage_key', 'usage_key',
'dyeable', 'dyeable',

View File

@@ -49,6 +49,9 @@ const changeLabelKeys: Record<string, string> = {
分类: 'pages.items.category', 分类: 'pages.items.category',
Usage: 'pages.items.usage', Usage: 'pages.items.usage',
用途: 'pages.items.usage', 用途: 'pages.items.usage',
'Base Price': 'pages.items.basePrice',
'Base price': 'pages.items.basePrice',
基础价格: 'pages.items.basePrice',
Dyeable: 'pages.items.dyeable', Dyeable: 'pages.items.dyeable',
可染色: 'pages.items.dyeable', 可染色: 'pages.items.dyeable',
'Dual dyeable': 'pages.items.dualDyeable', 'Dual dyeable': 'pages.items.dualDyeable',

View File

@@ -33,12 +33,14 @@ const props = withDefaults(
creating?: boolean; creating?: boolean;
createLabel?: string; createLabel?: string;
dropdownStrategy?: DropdownStrategy; dropdownStrategy?: DropdownStrategy;
clearable?: boolean;
}>(), }>(),
{ {
multiple: true, multiple: true,
max: 0, max: 0,
allowCreate: false, allowCreate: false,
creating: false creating: false,
clearable: false
} }
); );
@@ -167,6 +169,12 @@ function updateValue(values: string[]) {
function selectOption(value: string) { function selectOption(value: string) {
if (!props.multiple) { if (!props.multiple) {
if (props.clearable && selectedValues.value.has(value)) {
updateValue([]);
closeDropdown();
return;
}
updateValue([value]); updateValue([value]);
closeDropdown(); closeDropdown();
return; return;

View File

@@ -257,6 +257,7 @@ export interface Item extends EditInfo {
baseName?: string; baseName?: string;
details: string; details: string;
baseDetails?: string; baseDetails?: string;
basePrice: number | null;
isEventItem: boolean; isEventItem: boolean;
translations?: TranslationMap; translations?: TranslationMap;
image: EntityImage | null; image: EntityImage | null;
@@ -789,6 +790,7 @@ export interface PokemonImageOptionsResult {
export interface ItemPayload { export interface ItemPayload {
name: string; name: string;
details: string; details: string;
basePrice: number | null;
translations?: TranslationMap; translations?: TranslationMap;
categoryId: number; categoryId: number;
usageId: number | null; usageId: number | null;

View File

@@ -17,7 +17,7 @@ import { api, getAuthToken, type AuthUser, type ItemDetail } from '../services/a
import ItemEdit from './ItemEdit.vue'; import ItemEdit from './ItemEdit.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { locale, t } = useI18n();
const item = ref<ItemDetail | null>(null); const item = ref<ItemDetail | null>(null);
const currentUser = ref<AuthUser | null>(null); const currentUser = ref<AuthUser | null>(null);
const detailTab = ref('details'); const detailTab = ref('details');
@@ -38,6 +38,10 @@ const itemSubtitle = computed(() => {
}); });
const detailKicker = computed(() => (item.value?.isEventItem ? t('pages.eventItems.detailKicker') : t('pages.items.detailKicker'))); const detailKicker = computed(() => (item.value?.isEventItem ? t('pages.eventItems.detailKicker') : t('pages.items.detailKicker')));
const listTarget = computed(() => (item.value?.isEventItem ? '/event-items' : '/items')); const listTarget = computed(() => (item.value?.isEventItem ? '/event-items' : '/items'));
const basePriceDisplay = computed(() => {
const price = item.value?.basePrice;
return price === null || price === undefined ? t('common.none') : new Intl.NumberFormat(locale.value).format(price);
});
const customization = computed(() => { const customization = computed(() => {
if (!item.value) { if (!item.value) {
@@ -190,6 +194,10 @@ watch(
<dt>{{ t('pages.items.usage') }}</dt> <dt>{{ t('pages.items.usage') }}</dt>
<dd>{{ item.usage?.name ?? t('common.none') }}</dd> <dd>{{ item.usage?.name ?? t('common.none') }}</dd>
</div> </div>
<div>
<dt>{{ t('pages.items.basePrice') }}</dt>
<dd>{{ basePriceDisplay }}</dd>
</div>
<div> <div>
<dt>{{ t('pages.items.recipeInfo') }}</dt> <dt>{{ t('pages.items.recipeInfo') }}</dt>
<dd>{{ item.noRecipe ? t('pages.items.noRecipe') : item.recipe ? item.recipe.name : t('common.none') }}</dd> <dd>{{ item.noRecipe ? t('pages.items.noRecipe') : item.recipe ? item.recipe.name : t('common.none') }}</dd>

View File

@@ -38,6 +38,7 @@ const creatingSelect = ref('');
const itemForm = ref({ const itemForm = ref({
name: '', name: '',
details: '', details: '',
basePrice: '',
translations: {} as TranslationMap, translations: {} as TranslationMap,
categoryId: '', categoryId: '',
usageId: '', usageId: '',
@@ -53,6 +54,7 @@ const itemForm = ref({
type ItemCreateDefaults = { type ItemCreateDefaults = {
categoryId: string; categoryId: string;
usageId: string;
dyeable: boolean; dyeable: boolean;
dualDyeable: boolean; dualDyeable: boolean;
patternEditable: boolean; patternEditable: boolean;
@@ -98,6 +100,7 @@ function readItemCreateDefaults(): ItemCreateDefaults {
if (typeof sessionStorage === 'undefined') { if (typeof sessionStorage === 'undefined') {
return { return {
categoryId: '', categoryId: '',
usageId: '',
dyeable: false, dyeable: false,
dualDyeable: false, dualDyeable: false,
patternEditable: false, patternEditable: false,
@@ -111,6 +114,7 @@ function readItemCreateDefaults(): ItemCreateDefaults {
if (!rawValue) { if (!rawValue) {
return { return {
categoryId: '', categoryId: '',
usageId: '',
dyeable: false, dyeable: false,
dualDyeable: false, dualDyeable: false,
patternEditable: false, patternEditable: false,
@@ -122,6 +126,7 @@ function readItemCreateDefaults(): ItemCreateDefaults {
const parsedValue = JSON.parse(rawValue) as Partial<ItemCreateDefaults>; const parsedValue = JSON.parse(rawValue) as Partial<ItemCreateDefaults>;
return { return {
categoryId: typeof parsedValue.categoryId === 'string' ? parsedValue.categoryId : '', categoryId: typeof parsedValue.categoryId === 'string' ? parsedValue.categoryId : '',
usageId: typeof parsedValue.usageId === 'string' ? parsedValue.usageId : '',
dyeable: parsedValue.dyeable === true, dyeable: parsedValue.dyeable === true,
dualDyeable: parsedValue.dualDyeable === true, dualDyeable: parsedValue.dualDyeable === true,
patternEditable: parsedValue.patternEditable === true, patternEditable: parsedValue.patternEditable === true,
@@ -133,6 +138,7 @@ function readItemCreateDefaults(): ItemCreateDefaults {
} catch { } catch {
return { return {
categoryId: '', categoryId: '',
usageId: '',
dyeable: false, dyeable: false,
dualDyeable: false, dualDyeable: false,
patternEditable: false, patternEditable: false,
@@ -151,10 +157,12 @@ function applyItemCreateDefaults(isEventItem: boolean) {
const defaults = readItemCreateDefaults(); const defaults = readItemCreateDefaults();
const categoryIds = new Set(loadedOptions.itemCategories.map((item) => String(item.id))); const categoryIds = new Set(loadedOptions.itemCategories.map((item) => String(item.id)));
const usageIds = new Set(loadedOptions.itemUsages.map((item) => String(item.id)));
const methodIds = new Set(loadedOptions.acquisitionMethods.map((item) => String(item.id))); const methodIds = new Set(loadedOptions.acquisitionMethods.map((item) => String(item.id)));
itemForm.value = { itemForm.value = {
...itemForm.value, ...itemForm.value,
categoryId: categoryIds.has(defaults.categoryId) ? defaults.categoryId : '', categoryId: categoryIds.has(defaults.categoryId) ? defaults.categoryId : '',
usageId: usageIds.has(defaults.usageId) ? defaults.usageId : '',
dyeable: defaults.dyeable, dyeable: defaults.dyeable,
dualDyeable: defaults.dualDyeable, dualDyeable: defaults.dualDyeable,
patternEditable: defaults.patternEditable, patternEditable: defaults.patternEditable,
@@ -207,6 +215,7 @@ async function loadEditor() {
itemForm.value = { itemForm.value = {
name: item.baseName ?? item.name, name: item.baseName ?? item.name,
details: item.baseDetails ?? item.details, details: item.baseDetails ?? item.details,
basePrice: item.basePrice === null || item.basePrice === undefined ? '' : String(item.basePrice),
translations: item.translations ?? {}, translations: item.translations ?? {},
categoryId: String(item.category.id), categoryId: String(item.category.id),
usageId: item.usage ? String(item.usage.id) : '', usageId: item.usage ? String(item.usage.id) : '',
@@ -260,6 +269,7 @@ async function saveItem() {
const payload: ItemPayload = { const payload: ItemPayload = {
name: itemNameForSave(), name: itemNameForSave(),
details: itemForm.value.details, details: itemForm.value.details,
basePrice: itemForm.value.basePrice.trim() === '' ? null : Number(itemForm.value.basePrice),
translations: itemForm.value.translations, translations: itemForm.value.translations,
categoryId: Number(itemForm.value.categoryId), categoryId: Number(itemForm.value.categoryId),
usageId: itemForm.value.usageId ? Number(itemForm.value.usageId) : null, usageId: itemForm.value.usageId ? Number(itemForm.value.usageId) : null,
@@ -342,6 +352,11 @@ onMounted(() => {
@error="message = $event" @error="message = $event"
/> />
<div class="field">
<label for="item-base-price">{{ t('pages.items.basePrice') }}</label>
<input id="item-base-price" v-model="itemForm.basePrice" type="number" min="0" step="1" inputmode="numeric" />
</div>
<div class="field"> <div class="field">
<label for="item-category">{{ t('pages.items.category') }}</label> <label for="item-category">{{ t('pages.items.category') }}</label>
<TagsSelect <TagsSelect
@@ -361,6 +376,7 @@ onMounted(() => {
v-model="itemForm.usageId" v-model="itemForm.usageId"
:options="options.itemUsages" :options="options.itemUsages"
:multiple="false" :multiple="false"
clearable
:placeholder="t('common.none')" :placeholder="t('common.none')"
:search-placeholder="t('pages.items.searchUsage')" :search-placeholder="t('pages.items.searchUsage')"
/> />

View File

@@ -46,6 +46,7 @@ const dropCommitted = ref(false);
type ItemCreateDefaults = { type ItemCreateDefaults = {
categoryId: string; categoryId: string;
usageId: string;
dyeable: boolean; dyeable: boolean;
dualDyeable: boolean; dualDyeable: boolean;
patternEditable: boolean; patternEditable: boolean;
@@ -57,6 +58,7 @@ const itemCreateDefaultsStorageKey = 'pokopia_item_create_defaults';
const emptyItemCreateDefaults = (): ItemCreateDefaults => ({ const emptyItemCreateDefaults = (): ItemCreateDefaults => ({
categoryId: '', categoryId: '',
usageId: '',
dyeable: false, dyeable: false,
dualDyeable: false, dualDyeable: false,
patternEditable: false, patternEditable: false,
@@ -101,6 +103,7 @@ const canCreateItem = computed(() => currentUser.value?.permissions.includes('it
const hasItemCreateDefaults = computed( const hasItemCreateDefaults = computed(
() => () =>
itemCreateDefaults.value.categoryId !== '' || itemCreateDefaults.value.categoryId !== '' ||
itemCreateDefaults.value.usageId !== '' ||
itemCreateDefaults.value.dyeable || itemCreateDefaults.value.dyeable ||
itemCreateDefaults.value.dualDyeable || itemCreateDefaults.value.dualDyeable ||
itemCreateDefaults.value.patternEditable || itemCreateDefaults.value.patternEditable ||
@@ -177,6 +180,7 @@ function readItemCreateDefaults(): ItemCreateDefaults {
const parsedValue = JSON.parse(rawValue) as Partial<ItemCreateDefaults>; const parsedValue = JSON.parse(rawValue) as Partial<ItemCreateDefaults>;
return { return {
categoryId: typeof parsedValue.categoryId === 'string' ? parsedValue.categoryId : '', categoryId: typeof parsedValue.categoryId === 'string' ? parsedValue.categoryId : '',
usageId: typeof parsedValue.usageId === 'string' ? parsedValue.usageId : '',
dyeable: parsedValue.dyeable === true, dyeable: parsedValue.dyeable === true,
dualDyeable: parsedValue.dualDyeable === true, dualDyeable: parsedValue.dualDyeable === true,
patternEditable: parsedValue.patternEditable === true, patternEditable: parsedValue.patternEditable === true,
@@ -209,10 +213,12 @@ function sanitizeItemCreateDefaults() {
} }
const categoryIds = new Set(options.value.itemCategories.map((item) => String(item.id))); const categoryIds = new Set(options.value.itemCategories.map((item) => String(item.id)));
const usageIds = new Set(options.value.itemUsages.map((item) => String(item.id)));
const methodIds = new Set(options.value.acquisitionMethods.map((item) => String(item.id))); const methodIds = new Set(options.value.acquisitionMethods.map((item) => String(item.id)));
const nextDefaults = { const nextDefaults = {
...itemCreateDefaults.value, ...itemCreateDefaults.value,
categoryId: categoryIds.has(itemCreateDefaults.value.categoryId) ? itemCreateDefaults.value.categoryId : '', categoryId: categoryIds.has(itemCreateDefaults.value.categoryId) ? itemCreateDefaults.value.categoryId : '',
usageId: usageIds.has(itemCreateDefaults.value.usageId) ? itemCreateDefaults.value.usageId : '',
acquisitionMethodIds: itemCreateDefaults.value.acquisitionMethodIds.filter((item) => methodIds.has(item)) acquisitionMethodIds: itemCreateDefaults.value.acquisitionMethodIds.filter((item) => methodIds.has(item))
}; };
@@ -509,6 +515,19 @@ watch(itemSortingAllowed, (allowed) => {
/> />
</div> </div>
<div class="field">
<label for="item-default-usage">{{ t('pages.items.usage') }}</label>
<TagsSelect
id="item-default-usage"
v-model="itemCreateDefaults.usageId"
:options="options.itemUsages"
:multiple="false"
clearable
:placeholder="t('common.none')"
:search-placeholder="t('pages.items.searchUsage')"
/>
</div>
<div class="check-row item-create-defaults-menu__checks"> <div class="check-row item-create-defaults-menu__checks">
<label><input v-model="itemCreateDefaults.dyeable" type="checkbox" /> {{ t('pages.items.dyeable') }}</label> <label><input v-model="itemCreateDefaults.dyeable" type="checkbox" /> {{ t('pages.items.dyeable') }}</label>
<label><input v-model="itemCreateDefaults.dualDyeable" type="checkbox" /> {{ t('pages.items.dualDyeable') }}</label> <label><input v-model="itemCreateDefaults.dualDyeable" type="checkbox" /> {{ t('pages.items.dualDyeable') }}</label>
@@ -557,6 +576,7 @@ watch(itemSortingAllowed, (allowed) => {
v-model="usageId" v-model="usageId"
:options="options.itemUsages" :options="options.itemUsages"
:multiple="false" :multiple="false"
clearable
:placeholder="t('common.all')" :placeholder="t('common.all')"
:search-placeholder="t('pages.items.searchUsage')" :search-placeholder="t('pages.items.searchUsage')"
/> />

View File

@@ -134,7 +134,7 @@ export const systemWordingMessages = {
pokemonDetailDescription: pokemonDetailDescription:
'Read {name} details in Pokopia Wiki, including habitat, types, specialities, favourites, stats, related items, discussions, and edit history.', 'Read {name} details in Pokopia Wiki, including habitat, types, specialities, favourites, stats, related items, discussions, and edit history.',
itemDetailDescription: itemDetailDescription:
'Browse {name} item details in Pokopia Wiki, including category, usage, acquisition methods, customization, related recipes, habitats, and Pokemon drops.', 'Browse {name} item details in Pokopia Wiki, including base price, category, usage, acquisition methods, customization, related recipes, habitats, and Pokemon drops.',
ancientArtifactDetailDescription: ancientArtifactDetailDescription:
'Browse {name} Ancient Artifact details in Pokopia Wiki, including category, tags, description, discussions, and edit history.', 'Browse {name} Ancient Artifact details in Pokopia Wiki, including category, tags, description, discussions, and edit history.',
habitatDetailDescription: habitatDetailDescription:
@@ -680,7 +680,7 @@ export const systemWordingMessages = {
detailKicker: 'Item Detail', detailKicker: 'Item Detail',
detailSubtitle: 'Item detail', detailSubtitle: 'Item detail',
editKicker: 'Item Edit', editKicker: 'Item Edit',
editSubtitle: 'Maintain item category, usage, acquisition methods, customization, and tags.', editSubtitle: 'Maintain item base price, category, usage, acquisition methods, customization, and tags.',
newTitle: 'New item', newTitle: 'New item',
editTitle: 'Edit {name}', editTitle: 'Edit {name}',
fallbackName: 'Item', fallbackName: 'Item',
@@ -688,6 +688,7 @@ export const systemWordingMessages = {
loadingDetail: 'Loading item detail', loadingDetail: 'Loading item detail',
loadingEdit: 'Loading item editor', loadingEdit: 'Loading item editor',
description: 'Description', description: 'Description',
basePrice: 'Base Price',
category: 'Category', category: 'Category',
usage: 'Usage', usage: 'Usage',
tags: 'Tags', tags: 'Tags',
@@ -720,7 +721,7 @@ export const systemWordingMessages = {
subtitle: 'Browse event items by category, usage, and tags.', subtitle: 'Browse event items by category, usage, and tags.',
kicker: 'Event Items', kicker: 'Event Items',
detailKicker: 'Event Item Detail', detailKicker: 'Event Item Detail',
editSubtitle: 'Maintain event item category, usage, acquisition methods, customization, and tags.', editSubtitle: 'Maintain event item base price, category, usage, acquisition methods, customization, and tags.',
newTitle: 'New event item' newTitle: 'New event item'
}, },
ancientArtifacts: { ancientArtifacts: {
@@ -1495,7 +1496,7 @@ export const systemWordingMessages = {
seo: { seo: {
siteDescription: '浏览 Pokopia Wiki 的 Pokemon、Event Pokemon、栖息地、Event Habitats、物品、Event Items、Ancient Artifacts、材料单、每日清单和 Life 社区动态。', siteDescription: '浏览 Pokopia Wiki 的 Pokemon、Event Pokemon、栖息地、Event Habitats、物品、Event Items、Ancient Artifacts、材料单、每日清单和 Life 社区动态。',
pokemonDetailDescription: '查看 {name} 在 Pokopia Wiki 中的栖息地、属性、特长、喜欢的东西、六维、相关物品、讨论和编辑历史。', pokemonDetailDescription: '查看 {name} 在 Pokopia Wiki 中的栖息地、属性、特长、喜欢的东西、六维、相关物品、讨论和编辑历史。',
itemDetailDescription: '查看 {name} 在 Pokopia Wiki 中的分类、用途、入手方式、自定义、相关材料单、栖息地和 Pokemon 掉落。', itemDetailDescription: '查看 {name} 在 Pokopia Wiki 中的基础价格、分类、用途、入手方式、自定义、相关材料单、栖息地和 Pokemon 掉落。',
ancientArtifactDetailDescription: '查看 {name} 在 Pokopia Wiki 中的分类、标签、介绍、讨论和编辑历史。', ancientArtifactDetailDescription: '查看 {name} 在 Pokopia Wiki 中的分类、标签、介绍、讨论和编辑历史。',
habitatDetailDescription: '查看 {name} 在 Pokopia Wiki 中的配方、可能出现的 Pokemon、地图、时间、天气、讨论和编辑历史。', habitatDetailDescription: '查看 {name} 在 Pokopia Wiki 中的配方、可能出现的 Pokemon、地图、时间、天气、讨论和编辑历史。',
recipeDetailDescription: '查看 {name} 材料单在 Pokopia Wiki 中的结果物品、入手方式、需要材料、讨论和编辑历史。' recipeDetailDescription: '查看 {name} 材料单在 Pokopia Wiki 中的结果物品、入手方式、需要材料、讨论和编辑历史。'
@@ -2018,7 +2019,7 @@ export const systemWordingMessages = {
detailKicker: 'Item Detail', detailKicker: 'Item Detail',
detailSubtitle: '物品详情', detailSubtitle: '物品详情',
editKicker: 'Item Edit', editKicker: 'Item Edit',
editSubtitle: '维护物品分类、用途、入手方式、自定义和标签。', editSubtitle: '维护物品基础价格、分类、用途、入手方式、自定义和标签。',
newTitle: '新增物品', newTitle: '新增物品',
editTitle: '编辑 {name}', editTitle: '编辑 {name}',
fallbackName: '物品', fallbackName: '物品',
@@ -2026,6 +2027,7 @@ export const systemWordingMessages = {
loadingDetail: '正在加载物品详情', loadingDetail: '正在加载物品详情',
loadingEdit: '正在加载物品编辑内容', loadingEdit: '正在加载物品编辑内容',
description: '介绍', description: '介绍',
basePrice: '基础价格',
category: '分类', category: '分类',
usage: '用途', usage: '用途',
tags: '标签', tags: '标签',
@@ -2058,7 +2060,7 @@ export const systemWordingMessages = {
subtitle: '按分类、用途、标签查看活动物品。', subtitle: '按分类、用途、标签查看活动物品。',
kicker: 'Event Items', kicker: 'Event Items',
detailKicker: 'Event Item Detail', detailKicker: 'Event Item Detail',
editSubtitle: '维护 Event Item 分类、用途、入手方式、自定义和标签。', editSubtitle: '维护 Event Item 基础价格、分类、用途、入手方式、自定义和标签。',
newTitle: '新增 Event Item' newTitle: '新增 Event Item'
}, },
ancientArtifacts: { ancientArtifacts: {