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:
@@ -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
|
||||||
- 分类
|
- 分类
|
||||||
- 用途
|
- 用途
|
||||||
- 入手方式
|
- 入手方式
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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')"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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')"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user