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:
@@ -49,6 +49,9 @@ const changeLabelKeys: Record<string, string> = {
|
||||
分类: 'pages.items.category',
|
||||
Usage: 'pages.items.usage',
|
||||
用途: 'pages.items.usage',
|
||||
'Base Price': 'pages.items.basePrice',
|
||||
'Base price': 'pages.items.basePrice',
|
||||
基础价格: 'pages.items.basePrice',
|
||||
Dyeable: 'pages.items.dyeable',
|
||||
可染色: 'pages.items.dyeable',
|
||||
'Dual dyeable': 'pages.items.dualDyeable',
|
||||
|
||||
@@ -33,12 +33,14 @@ const props = withDefaults(
|
||||
creating?: boolean;
|
||||
createLabel?: string;
|
||||
dropdownStrategy?: DropdownStrategy;
|
||||
clearable?: boolean;
|
||||
}>(),
|
||||
{
|
||||
multiple: true,
|
||||
max: 0,
|
||||
allowCreate: false,
|
||||
creating: false
|
||||
creating: false,
|
||||
clearable: false
|
||||
}
|
||||
);
|
||||
|
||||
@@ -167,6 +169,12 @@ function updateValue(values: string[]) {
|
||||
|
||||
function selectOption(value: string) {
|
||||
if (!props.multiple) {
|
||||
if (props.clearable && selectedValues.value.has(value)) {
|
||||
updateValue([]);
|
||||
closeDropdown();
|
||||
return;
|
||||
}
|
||||
|
||||
updateValue([value]);
|
||||
closeDropdown();
|
||||
return;
|
||||
|
||||
@@ -257,6 +257,7 @@ export interface Item extends EditInfo {
|
||||
baseName?: string;
|
||||
details: string;
|
||||
baseDetails?: string;
|
||||
basePrice: number | null;
|
||||
isEventItem: boolean;
|
||||
translations?: TranslationMap;
|
||||
image: EntityImage | null;
|
||||
@@ -789,6 +790,7 @@ export interface PokemonImageOptionsResult {
|
||||
export interface ItemPayload {
|
||||
name: string;
|
||||
details: string;
|
||||
basePrice: number | null;
|
||||
translations?: TranslationMap;
|
||||
categoryId: number;
|
||||
usageId: number | null;
|
||||
|
||||
@@ -17,7 +17,7 @@ import { api, getAuthToken, type AuthUser, type ItemDetail } from '../services/a
|
||||
import ItemEdit from './ItemEdit.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
const { locale, t } = useI18n();
|
||||
const item = ref<ItemDetail | null>(null);
|
||||
const currentUser = ref<AuthUser | null>(null);
|
||||
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 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(() => {
|
||||
if (!item.value) {
|
||||
@@ -190,6 +194,10 @@ watch(
|
||||
<dt>{{ t('pages.items.usage') }}</dt>
|
||||
<dd>{{ item.usage?.name ?? t('common.none') }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ t('pages.items.basePrice') }}</dt>
|
||||
<dd>{{ basePriceDisplay }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{{ t('pages.items.recipeInfo') }}</dt>
|
||||
<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({
|
||||
name: '',
|
||||
details: '',
|
||||
basePrice: '',
|
||||
translations: {} as TranslationMap,
|
||||
categoryId: '',
|
||||
usageId: '',
|
||||
@@ -53,6 +54,7 @@ const itemForm = ref({
|
||||
|
||||
type ItemCreateDefaults = {
|
||||
categoryId: string;
|
||||
usageId: string;
|
||||
dyeable: boolean;
|
||||
dualDyeable: boolean;
|
||||
patternEditable: boolean;
|
||||
@@ -98,6 +100,7 @@ function readItemCreateDefaults(): ItemCreateDefaults {
|
||||
if (typeof sessionStorage === 'undefined') {
|
||||
return {
|
||||
categoryId: '',
|
||||
usageId: '',
|
||||
dyeable: false,
|
||||
dualDyeable: false,
|
||||
patternEditable: false,
|
||||
@@ -111,6 +114,7 @@ function readItemCreateDefaults(): ItemCreateDefaults {
|
||||
if (!rawValue) {
|
||||
return {
|
||||
categoryId: '',
|
||||
usageId: '',
|
||||
dyeable: false,
|
||||
dualDyeable: false,
|
||||
patternEditable: false,
|
||||
@@ -122,6 +126,7 @@ function readItemCreateDefaults(): ItemCreateDefaults {
|
||||
const parsedValue = JSON.parse(rawValue) as Partial<ItemCreateDefaults>;
|
||||
return {
|
||||
categoryId: typeof parsedValue.categoryId === 'string' ? parsedValue.categoryId : '',
|
||||
usageId: typeof parsedValue.usageId === 'string' ? parsedValue.usageId : '',
|
||||
dyeable: parsedValue.dyeable === true,
|
||||
dualDyeable: parsedValue.dualDyeable === true,
|
||||
patternEditable: parsedValue.patternEditable === true,
|
||||
@@ -133,6 +138,7 @@ function readItemCreateDefaults(): ItemCreateDefaults {
|
||||
} catch {
|
||||
return {
|
||||
categoryId: '',
|
||||
usageId: '',
|
||||
dyeable: false,
|
||||
dualDyeable: false,
|
||||
patternEditable: false,
|
||||
@@ -151,10 +157,12 @@ function applyItemCreateDefaults(isEventItem: boolean) {
|
||||
|
||||
const defaults = readItemCreateDefaults();
|
||||
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)));
|
||||
itemForm.value = {
|
||||
...itemForm.value,
|
||||
categoryId: categoryIds.has(defaults.categoryId) ? defaults.categoryId : '',
|
||||
usageId: usageIds.has(defaults.usageId) ? defaults.usageId : '',
|
||||
dyeable: defaults.dyeable,
|
||||
dualDyeable: defaults.dualDyeable,
|
||||
patternEditable: defaults.patternEditable,
|
||||
@@ -207,6 +215,7 @@ async function loadEditor() {
|
||||
itemForm.value = {
|
||||
name: item.baseName ?? item.name,
|
||||
details: item.baseDetails ?? item.details,
|
||||
basePrice: item.basePrice === null || item.basePrice === undefined ? '' : String(item.basePrice),
|
||||
translations: item.translations ?? {},
|
||||
categoryId: String(item.category.id),
|
||||
usageId: item.usage ? String(item.usage.id) : '',
|
||||
@@ -260,6 +269,7 @@ async function saveItem() {
|
||||
const payload: ItemPayload = {
|
||||
name: itemNameForSave(),
|
||||
details: itemForm.value.details,
|
||||
basePrice: itemForm.value.basePrice.trim() === '' ? null : Number(itemForm.value.basePrice),
|
||||
translations: itemForm.value.translations,
|
||||
categoryId: Number(itemForm.value.categoryId),
|
||||
usageId: itemForm.value.usageId ? Number(itemForm.value.usageId) : null,
|
||||
@@ -342,6 +352,11 @@ onMounted(() => {
|
||||
@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">
|
||||
<label for="item-category">{{ t('pages.items.category') }}</label>
|
||||
<TagsSelect
|
||||
@@ -361,6 +376,7 @@ onMounted(() => {
|
||||
v-model="itemForm.usageId"
|
||||
:options="options.itemUsages"
|
||||
:multiple="false"
|
||||
clearable
|
||||
:placeholder="t('common.none')"
|
||||
:search-placeholder="t('pages.items.searchUsage')"
|
||||
/>
|
||||
|
||||
@@ -46,6 +46,7 @@ const dropCommitted = ref(false);
|
||||
|
||||
type ItemCreateDefaults = {
|
||||
categoryId: string;
|
||||
usageId: string;
|
||||
dyeable: boolean;
|
||||
dualDyeable: boolean;
|
||||
patternEditable: boolean;
|
||||
@@ -57,6 +58,7 @@ const itemCreateDefaultsStorageKey = 'pokopia_item_create_defaults';
|
||||
|
||||
const emptyItemCreateDefaults = (): ItemCreateDefaults => ({
|
||||
categoryId: '',
|
||||
usageId: '',
|
||||
dyeable: false,
|
||||
dualDyeable: false,
|
||||
patternEditable: false,
|
||||
@@ -101,6 +103,7 @@ const canCreateItem = computed(() => currentUser.value?.permissions.includes('it
|
||||
const hasItemCreateDefaults = computed(
|
||||
() =>
|
||||
itemCreateDefaults.value.categoryId !== '' ||
|
||||
itemCreateDefaults.value.usageId !== '' ||
|
||||
itemCreateDefaults.value.dyeable ||
|
||||
itemCreateDefaults.value.dualDyeable ||
|
||||
itemCreateDefaults.value.patternEditable ||
|
||||
@@ -177,6 +180,7 @@ function readItemCreateDefaults(): ItemCreateDefaults {
|
||||
const parsedValue = JSON.parse(rawValue) as Partial<ItemCreateDefaults>;
|
||||
return {
|
||||
categoryId: typeof parsedValue.categoryId === 'string' ? parsedValue.categoryId : '',
|
||||
usageId: typeof parsedValue.usageId === 'string' ? parsedValue.usageId : '',
|
||||
dyeable: parsedValue.dyeable === true,
|
||||
dualDyeable: parsedValue.dualDyeable === true,
|
||||
patternEditable: parsedValue.patternEditable === true,
|
||||
@@ -209,10 +213,12 @@ function sanitizeItemCreateDefaults() {
|
||||
}
|
||||
|
||||
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 nextDefaults = {
|
||||
...itemCreateDefaults.value,
|
||||
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))
|
||||
};
|
||||
|
||||
@@ -509,6 +515,19 @@ watch(itemSortingAllowed, (allowed) => {
|
||||
/>
|
||||
</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">
|
||||
<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>
|
||||
@@ -557,6 +576,7 @@ watch(itemSortingAllowed, (allowed) => {
|
||||
v-model="usageId"
|
||||
:options="options.itemUsages"
|
||||
:multiple="false"
|
||||
clearable
|
||||
:placeholder="t('common.all')"
|
||||
:search-placeholder="t('pages.items.searchUsage')"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user