feat(items): replace dyeable booleans with dyeability level

Add dyeability integer field to support up to triple dyeable items
Update frontend forms to use a radio group for dyeability selection
This commit is contained in:
2026-05-07 10:17:45 +08:00
parent 515297ab74
commit 23a7301598
10 changed files with 230 additions and 100 deletions

View File

@@ -645,8 +645,11 @@ Pokemon 详情页展示:
- Road - Road
- 入手方式:可多选 - 入手方式:可多选
- 客制化: - 客制化:
- 染色 - 染色能力:`dyeability`,使用互斥枚举值维护:
-双区染色 - `0`:不可染色
- `1`:可染色
- `2`:可双区染色
- `3`:可三区染色
- 可改花纹 - 可改花纹
- 无材料单:`no_recipe` - 无材料单:`no_recipe`
- 标签:使用喜欢的东西配置,可多选 - 标签:使用喜欢的东西配置,可多选

View File

@@ -905,6 +905,7 @@ CREATE TABLE IF NOT EXISTS items (
ancient_artifact_category_key text, ancient_artifact_category_key text,
category_key text NOT NULL DEFAULT 'other', category_key text NOT NULL DEFAULT 'other',
usage_key text, usage_key text,
dyeability integer NOT NULL DEFAULT 0 CHECK (dyeability IN (0, 1, 2, 3)),
dyeable boolean NOT NULL DEFAULT false, dyeable boolean NOT NULL DEFAULT false,
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,
@@ -1276,3 +1277,15 @@ CREATE INDEX IF NOT EXISTS entity_discussion_comments_ai_moderation_language_idx
ALTER TABLE skills ALTER TABLE skills
ADD COLUMN IF NOT EXISTS has_trading boolean NOT NULL DEFAULT false; ADD COLUMN IF NOT EXISTS has_trading boolean NOT NULL DEFAULT false;
ALTER TABLE items
ADD COLUMN IF NOT EXISTS dyeability integer NOT NULL DEFAULT 0 CHECK (dyeability IN (0, 1, 2, 3));
UPDATE items
SET dyeability = CASE
WHEN dual_dyeable THEN 2
WHEN dyeable THEN 1
ELSE 0
END
WHERE dyeability = 0
AND (dual_dyeable OR dyeable);

View File

@@ -223,8 +223,7 @@ type ItemPayload = {
categoryKey: string; categoryKey: string;
usageId: number | null; usageId: number | null;
usageKey: string | null; usageKey: string | null;
dyeable: boolean; dyeability: number;
dualDyeable: boolean;
patternEditable: boolean; patternEditable: boolean;
noRecipe: boolean; noRecipe: boolean;
isEventItem: boolean; isEventItem: boolean;
@@ -565,7 +564,7 @@ type ItemChangeSource = {
image: EntityImageValue | null; image: EntityImageValue | null;
category: { name: string }; category: { name: string };
usage: { name: string } | null; usage: { name: string } | null;
customization: { dyeable: boolean; dualDyeable: boolean; patternEditable: boolean }; customization: { dyeability: number; patternEditable: boolean };
noRecipe: boolean; noRecipe: boolean;
acquisitionMethods: Array<{ name: string }>; acquisitionMethods: Array<{ name: string }>;
tags: Array<{ name: string }>; tags: Array<{ name: string }>;
@@ -2436,8 +2435,7 @@ async function itemEditChanges(
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, systemListNameByKey(itemCategoryOptions, after.categoryKey)); pushChange(changes, 'Category', before.category.name, systemListNameByKey(itemCategoryOptions, after.categoryKey));
pushChange(changes, 'Usage', before.usage?.name, systemListNameByKey(itemUsageOptions, after.usageKey)); pushChange(changes, 'Usage', before.usage?.name, systemListNameByKey(itemUsageOptions, after.usageKey));
pushChange(changes, 'Dyeable', boolValue(before.customization.dyeable), boolValue(after.dyeable)); pushChange(changes, 'Dyeability', dyeabilityValue(before.customization.dyeability), dyeabilityValue(after.dyeability));
pushChange(changes, 'Dual dyeable', boolValue(before.customization.dualDyeable), boolValue(after.dualDyeable));
pushChange(changes, 'Pattern editable', boolValue(before.customization.patternEditable), boolValue(after.patternEditable)); pushChange(changes, 'Pattern editable', boolValue(before.customization.patternEditable), boolValue(after.patternEditable));
pushChange(changes, 'No recipe', boolValue(before.noRecipe), boolValue(after.noRecipe)); pushChange(changes, 'No recipe', boolValue(before.noRecipe), boolValue(after.noRecipe));
pushChange(changes, 'Acquisition methods', namedListValue(before.acquisitionMethods), namesFromIds(after.acquisitionMethodIds, methodNames)); pushChange(changes, 'Acquisition methods', namedListValue(before.acquisitionMethods), namesFromIds(after.acquisitionMethodIds, methodNames));
@@ -6597,8 +6595,7 @@ function itemProjection(locale: string): string {
ELSE ${systemListJsonSql('i.usage_key', itemUsageOptions, locale)} ELSE ${systemListJsonSql('i.usage_key', itemUsageOptions, locale)}
END AS usage, END AS usage,
json_build_object( json_build_object(
'dyeable', i.dyeable, 'dyeability', i.dyeability,
'dualDyeable', i.dual_dyeable,
'patternEditable', i.pattern_editable 'patternEditable', i.pattern_editable
) AS customization, ) AS customization,
i.no_recipe AS "noRecipe", i.no_recipe AS "noRecipe",
@@ -6928,8 +6925,7 @@ function cleanItemPayload(payload: Record<string, unknown>): ItemPayload {
categoryKey: category.key, categoryKey: category.key,
usageId, usageId,
usageKey: usage?.key ?? null, usageKey: usage?.key ?? null,
dyeable: Boolean(payload.dyeable), dyeability: cleanDyeability(payload),
dualDyeable: Boolean(payload.dualDyeable),
patternEditable: Boolean(payload.patternEditable), patternEditable: Boolean(payload.patternEditable),
noRecipe: Boolean(payload.noRecipe), noRecipe: Boolean(payload.noRecipe),
isEventItem: Boolean(payload.isEventItem), isEventItem: Boolean(payload.isEventItem),
@@ -6949,6 +6945,38 @@ function cleanOptionalPositiveInteger(value: unknown): number | null {
return requirePositiveInteger(value, 'server.validation.invalidField'); return requirePositiveInteger(value, 'server.validation.invalidField');
} }
function cleanDyeability(payload: Record<string, unknown>): number {
if (payload.dyeability === undefined || payload.dyeability === null || payload.dyeability === '') {
if (payload.dualDyeable === true) {
return 2;
}
if (payload.dyeable === true) {
return 1;
}
return 0;
}
const dyeability = Number(payload.dyeability);
if (!Number.isInteger(dyeability) || dyeability < 0 || dyeability > 3) {
throw validationError('server.validation.invalidField');
}
return dyeability;
}
function dyeabilityValue(value: number): string {
if (value === 3) {
return 'Triple dyeable';
}
if (value === 2) {
return 'Dual dyeable';
}
if (value === 1) {
return 'Dyeable';
}
return 'Not dyeable';
}
async function orderedItemIds(client: DbClient, isEventItem: boolean): Promise<number[]> { async function orderedItemIds(client: DbClient, isEventItem: boolean): Promise<number[]> {
const rows = await client.query<{ id: number }>( const rows = await client.query<{ id: number }>(
'SELECT id FROM items WHERE is_event_item = $1 ORDER BY sort_order, id', 'SELECT id FROM items WHERE is_event_item = $1 ORDER BY sort_order, id',
@@ -7001,6 +7029,7 @@ export async function createItem(payload: Record<string, unknown>, userId: numbe
base_price, base_price,
category_key, category_key,
usage_key, usage_key,
dyeability,
dyeable, dyeable,
dual_dyeable, dual_dyeable,
pattern_editable, pattern_editable,
@@ -7011,7 +7040,7 @@ 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, $13, $14, $14) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $15)
RETURNING id RETURNING id
`, `,
[ [
@@ -7021,8 +7050,9 @@ export async function createItem(payload: Record<string, unknown>, userId: numbe
cleanPayload.basePrice, cleanPayload.basePrice,
cleanPayload.categoryKey, cleanPayload.categoryKey,
cleanPayload.usageKey, cleanPayload.usageKey,
cleanPayload.dyeable, cleanPayload.dyeability,
cleanPayload.dualDyeable, cleanPayload.dyeability >= 1,
cleanPayload.dyeability >= 2,
cleanPayload.patternEditable, cleanPayload.patternEditable,
cleanPayload.noRecipe, cleanPayload.noRecipe,
cleanPayload.isEventItem, cleanPayload.isEventItem,
@@ -7078,15 +7108,16 @@ export async function updateItem(id: number, payload: Record<string, unknown>, u
base_price = $4, base_price = $4,
category_key = $5, category_key = $5,
usage_key = $6, usage_key = $6,
dyeable = $7, dyeability = $7,
dual_dyeable = $8, dyeable = $8,
pattern_editable = $9, dual_dyeable = $9,
no_recipe = $10, pattern_editable = $10,
is_event_item = $11, no_recipe = $11,
image_path = $12, is_event_item = $12,
updated_by_user_id = $13, image_path = $13,
updated_by_user_id = $14,
updated_at = now() updated_at = now()
WHERE id = $14 WHERE id = $15
`, `,
[ [
cleanPayload.name, cleanPayload.name,
@@ -7095,8 +7126,9 @@ export async function updateItem(id: number, payload: Record<string, unknown>, u
cleanPayload.basePrice, cleanPayload.basePrice,
cleanPayload.categoryKey, cleanPayload.categoryKey,
cleanPayload.usageKey, cleanPayload.usageKey,
cleanPayload.dyeable, cleanPayload.dyeability,
cleanPayload.dualDyeable, cleanPayload.dyeability >= 1,
cleanPayload.dyeability >= 2,
cleanPayload.patternEditable, cleanPayload.patternEditable,
cleanPayload.noRecipe, cleanPayload.noRecipe,
cleanPayload.isEventItem, cleanPayload.isEventItem,
@@ -8041,6 +8073,7 @@ const dataToolColumns = {
'ancient_artifact_category_key', 'ancient_artifact_category_key',
'category_key', 'category_key',
'usage_key', 'usage_key',
'dyeability',
'dyeable', 'dyeable',
'dual_dyeable', 'dual_dyeable',
'pattern_editable', 'pattern_editable',
@@ -8306,6 +8339,16 @@ function normalizeImportValue(value: unknown): unknown {
} }
function normalizeImportColumnValue(row: Record<string, unknown>, column: string): unknown { function normalizeImportColumnValue(row: Record<string, unknown>, column: string): unknown {
if (column === 'dyeability' && row[column] === undefined) {
if (row.dual_dyeable === true) {
return 2;
}
if (row.dyeable === true) {
return 1;
}
return 0;
}
return normalizeImportValue(row[column]); return normalizeImportValue(row[column]);
} }

View File

@@ -55,10 +55,14 @@ const changeLabelKeys: Record<string, string> = {
'Base Price': 'pages.items.basePrice', 'Base Price': 'pages.items.basePrice',
'Base price': 'pages.items.basePrice', 'Base price': 'pages.items.basePrice',
基础价格: 'pages.items.basePrice', 基础价格: 'pages.items.basePrice',
Dyeability: 'pages.items.dyeability',
染色能力: 'pages.items.dyeability',
Dyeable: 'pages.items.dyeable', Dyeable: 'pages.items.dyeable',
可染色: 'pages.items.dyeable', 可染色: 'pages.items.dyeable',
'Dual dyeable': 'pages.items.dualDyeable', 'Dual dyeable': 'pages.items.dualDyeable',
可双区染色: 'pages.items.dualDyeable', 可双区染色: 'pages.items.dualDyeable',
'Triple dyeable': 'pages.items.tripleDyeable',
可三区染色: 'pages.items.tripleDyeable',
'Pattern editable': 'pages.items.patternEditable', 'Pattern editable': 'pages.items.patternEditable',
可改花纹: 'pages.items.patternEditable', 可改花纹: 'pages.items.patternEditable',
'No recipe': 'pages.items.noRecipe', 'No recipe': 'pages.items.noRecipe',
@@ -117,6 +121,14 @@ function changeValue(value: string): string {
const values: Record<string, string> = { const values: Record<string, string> = {
None: t('common.none'), None: t('common.none'),
: t('common.none'), : t('common.none'),
'Not dyeable': t('pages.items.notDyeable'),
不可染色: t('pages.items.notDyeable'),
Dyeable: t('pages.items.dyeable'),
可染色: t('pages.items.dyeable'),
'Dual dyeable': t('pages.items.dualDyeable'),
可双区染色: t('pages.items.dualDyeable'),
'Triple dyeable': t('pages.items.tripleDyeable'),
可三区染色: t('pages.items.tripleDyeable'),
Yes: locale.value === 'zh-CN' ? '是' : 'Yes', Yes: locale.value === 'zh-CN' ? '是' : 'Yes',
: locale.value === 'zh-CN' ? '是' : 'Yes', : locale.value === 'zh-CN' ? '是' : 'Yes',
No: locale.value === 'zh-CN' ? '否' : 'No', No: locale.value === 'zh-CN' ? '否' : 'No',

View File

@@ -325,8 +325,7 @@ export interface Item extends EditInfo {
category: NamedEntity; category: NamedEntity;
usage: NamedEntity | null; usage: NamedEntity | null;
customization: { customization: {
dyeable: boolean; dyeability: number;
dualDyeable: boolean;
patternEditable: boolean; patternEditable: boolean;
}; };
noRecipe: boolean; noRecipe: boolean;
@@ -873,8 +872,7 @@ export interface ItemPayload {
translations?: TranslationMap; translations?: TranslationMap;
categoryId: number; categoryId: number;
usageId: number | null; usageId: number | null;
dyeable: boolean; dyeability: number;
dualDyeable: boolean;
patternEditable: boolean; patternEditable: boolean;
noRecipe: boolean; noRecipe: boolean;
isEventItem: boolean; isEventItem: boolean;

View File

@@ -7477,6 +7477,46 @@ button:disabled,
align-items: center; align-items: center;
} }
.radio-group {
display: grid;
gap: 7px;
min-width: 0;
min-inline-size: 0;
margin: 0;
padding: 0;
border: 0;
}
.radio-group legend {
padding: 0;
color: var(--ink-soft);
font-size: 14px;
font-weight: 850;
}
.radio-group__options {
display: flex;
flex-wrap: wrap;
gap: 8px 12px;
align-items: center;
}
.radio-group__option {
display: inline-flex;
align-items: center;
gap: 7px;
min-height: 36px;
color: var(--ink-soft);
font-weight: 850;
cursor: pointer;
}
.radio-group__option input {
width: 18px;
height: 18px;
accent-color: var(--pokemon-blue);
}
.row-actions { .row-actions {
flex: 0 0 auto; flex: 0 0 auto;
flex-wrap: wrap; flex-wrap: wrap;

View File

@@ -117,9 +117,14 @@ const customization = computed(() => {
return []; return [];
} }
const dyeabilityLabels: Record<number, string> = {
1: t('pages.items.dyeable'),
2: t('pages.items.dualDyeable'),
3: t('pages.items.tripleDyeable')
};
return [ return [
item.value.customization.dyeable ? t('pages.items.dyeable') : '', dyeabilityLabels[item.value.customization.dyeability] ?? '',
item.value.customization.dualDyeable ? t('pages.items.dualDyeable') : '',
item.value.customization.patternEditable ? t('pages.items.patternEditable') : '' item.value.customization.patternEditable ? t('pages.items.patternEditable') : ''
].filter(Boolean); ].filter(Boolean);
}); });

View File

@@ -34,6 +34,9 @@ const loading = ref(true);
const busy = ref(false); const busy = ref(false);
const message = ref(''); const message = ref('');
const creatingSelect = ref(''); const creatingSelect = ref('');
type Dyeability = 0 | 1 | 2 | 3;
const itemForm = ref({ const itemForm = ref({
name: '', name: '',
details: '', details: '',
@@ -42,8 +45,7 @@ const itemForm = ref({
translations: {} as TranslationMap, translations: {} as TranslationMap,
categoryId: '', categoryId: '',
usageId: '', usageId: '',
dyeable: false, dyeability: 0 as Dyeability,
dualDyeable: false,
patternEditable: false, patternEditable: false,
noRecipe: false, noRecipe: false,
isEventItem: false, isEventItem: false,
@@ -55,8 +57,7 @@ const itemForm = ref({
type ItemCreateDefaults = { type ItemCreateDefaults = {
categoryId: string; categoryId: string;
usageId: string; usageId: string;
dyeable: boolean; dyeability: Dyeability;
dualDyeable: boolean;
patternEditable: boolean; patternEditable: boolean;
noRecipe: boolean; noRecipe: boolean;
acquisitionMethodIds: string[]; acquisitionMethodIds: string[];
@@ -100,6 +101,12 @@ const ancientArtifactOptions = computed(() => [
{ value: '', label: t('common.no') }, { value: '', label: t('common.no') },
...(options.value?.ancientArtifactCategories.map((item) => ({ value: String(item.id), label: item.name })) ?? []) ...(options.value?.ancientArtifactCategories.map((item) => ({ value: String(item.id), label: item.name })) ?? [])
]); ]);
const dyeabilityOptions = computed<Array<{ value: Dyeability; label: string }>>(() => [
{ value: 0, label: t('pages.items.notDyeable') },
{ value: 1, label: t('pages.items.dyeable') },
{ value: 2, label: t('pages.items.dualDyeable') },
{ value: 3, label: t('pages.items.tripleDyeable') }
]);
const canCreateConfig = computed(() => currentUser.value?.permissions.includes('admin.config.create') === true); const canCreateConfig = computed(() => currentUser.value?.permissions.includes('admin.config.create') === true);
const canUploadImage = computed(() => currentUser.value?.permissions.includes('items.upload') === true); const canUploadImage = computed(() => currentUser.value?.permissions.includes('items.upload') === true);
@@ -117,13 +124,26 @@ function errorText(error: unknown, fallback: string) {
return error instanceof Error && error.message ? error.message : fallback; return error instanceof Error && error.message ? error.message : fallback;
} }
function defaultDyeability(value: { dyeability?: unknown; dualDyeable?: unknown; dyeable?: unknown }): Dyeability {
const dyeability = Number(value.dyeability);
if (Number.isInteger(dyeability) && dyeability >= 0 && dyeability <= 3) {
return dyeability as Dyeability;
}
if (value.dualDyeable === true) {
return 2;
}
if (value.dyeable === true) {
return 1;
}
return 0;
}
function readItemCreateDefaults(): ItemCreateDefaults { function readItemCreateDefaults(): ItemCreateDefaults {
if (typeof sessionStorage === 'undefined') { if (typeof sessionStorage === 'undefined') {
return { return {
categoryId: '', categoryId: '',
usageId: '', usageId: '',
dyeable: false, dyeability: 0,
dualDyeable: false,
patternEditable: false, patternEditable: false,
noRecipe: false, noRecipe: false,
acquisitionMethodIds: [] acquisitionMethodIds: []
@@ -136,8 +156,7 @@ function readItemCreateDefaults(): ItemCreateDefaults {
return { return {
categoryId: '', categoryId: '',
usageId: '', usageId: '',
dyeable: false, dyeability: 0,
dualDyeable: false,
patternEditable: false, patternEditable: false,
noRecipe: false, noRecipe: false,
acquisitionMethodIds: [] acquisitionMethodIds: []
@@ -148,8 +167,7 @@ function readItemCreateDefaults(): ItemCreateDefaults {
return { return {
categoryId: typeof parsedValue.categoryId === 'string' ? parsedValue.categoryId : '', categoryId: typeof parsedValue.categoryId === 'string' ? parsedValue.categoryId : '',
usageId: typeof parsedValue.usageId === 'string' ? parsedValue.usageId : '', usageId: typeof parsedValue.usageId === 'string' ? parsedValue.usageId : '',
dyeable: parsedValue.dyeable === true, dyeability: defaultDyeability(parsedValue),
dualDyeable: parsedValue.dualDyeable === true,
patternEditable: parsedValue.patternEditable === true, patternEditable: parsedValue.patternEditable === true,
noRecipe: parsedValue.noRecipe === true, noRecipe: parsedValue.noRecipe === true,
acquisitionMethodIds: Array.isArray(parsedValue.acquisitionMethodIds) acquisitionMethodIds: Array.isArray(parsedValue.acquisitionMethodIds)
@@ -160,8 +178,7 @@ function readItemCreateDefaults(): ItemCreateDefaults {
return { return {
categoryId: '', categoryId: '',
usageId: '', usageId: '',
dyeable: false, dyeability: 0,
dualDyeable: false,
patternEditable: false, patternEditable: false,
noRecipe: false, noRecipe: false,
acquisitionMethodIds: [] acquisitionMethodIds: []
@@ -185,8 +202,7 @@ function applyItemCreateDefaults(isEventItem: boolean) {
categoryId: categoryIds.has(defaults.categoryId) ? defaults.categoryId : '', categoryId: categoryIds.has(defaults.categoryId) ? defaults.categoryId : '',
usageId: usageIds.has(defaults.usageId) ? defaults.usageId : '', usageId: usageIds.has(defaults.usageId) ? defaults.usageId : '',
ancientArtifactCategoryId: isAncientArtifactCreate.value ? String(loadedOptions.ancientArtifactCategories[0]?.id ?? '') : '', ancientArtifactCategoryId: isAncientArtifactCreate.value ? String(loadedOptions.ancientArtifactCategories[0]?.id ?? '') : '',
dyeable: defaults.dyeable, dyeability: defaults.dyeability,
dualDyeable: defaults.dualDyeable,
patternEditable: defaults.patternEditable, patternEditable: defaults.patternEditable,
noRecipe: defaults.noRecipe, noRecipe: defaults.noRecipe,
isEventItem, isEventItem,
@@ -237,8 +253,7 @@ async function loadEditor() {
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) : '',
dyeable: item.customization.dyeable, dyeability: defaultDyeability(item.customization),
dualDyeable: item.customization.dualDyeable,
patternEditable: item.customization.patternEditable, patternEditable: item.customization.patternEditable,
noRecipe: item.noRecipe, noRecipe: item.noRecipe,
isEventItem: item.isEventItem, isEventItem: item.isEventItem,
@@ -293,8 +308,7 @@ async function saveItem() {
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,
dyeable: itemForm.value.dyeable, dyeability: itemForm.value.dyeability,
dualDyeable: itemForm.value.dualDyeable,
patternEditable: itemForm.value.patternEditable, patternEditable: itemForm.value.patternEditable,
noRecipe: itemForm.value.noRecipe, noRecipe: itemForm.value.noRecipe,
isEventItem: itemForm.value.isEventItem, isEventItem: itemForm.value.isEventItem,
@@ -418,9 +432,19 @@ onMounted(() => {
</fieldset> </fieldset>
</div> </div>
<div class="field">
<fieldset class="radio-group">
<legend>{{ t('pages.items.dyeability') }}</legend>
<div class="radio-group__options">
<label v-for="option in dyeabilityOptions" :key="option.value" class="radio-group__option">
<input v-model="itemForm.dyeability" type="radio" name="item-dyeability" :value="option.value" />
<span>{{ option.label }}</span>
</label>
</div>
</fieldset>
</div>
<div class="check-row"> <div class="check-row">
<label><input v-model="itemForm.dyeable" type="checkbox" /> {{ t('pages.items.dyeable') }}</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" :disabled="isEventCreate" /> {{ t('pages.items.eventItem') }}</label> <label><input v-model="itemForm.isEventItem" type="checkbox" :disabled="isEventCreate" /> {{ t('pages.items.eventItem') }}</label>
@@ -492,46 +516,6 @@ onMounted(() => {
min-width: 0; min-width: 0;
} }
.radio-group {
display: grid;
gap: 7px;
min-width: 0;
min-inline-size: 0;
margin: 0;
padding: 0;
border: 0;
}
.radio-group legend {
padding: 0;
color: var(--ink-soft);
font-size: 14px;
font-weight: 850;
}
.radio-group__options {
display: flex;
flex-wrap: wrap;
gap: 8px 12px;
align-items: center;
}
.radio-group__option {
display: inline-flex;
align-items: center;
gap: 7px;
min-height: 36px;
color: var(--ink-soft);
font-weight: 850;
cursor: pointer;
}
.radio-group__option input {
width: 18px;
height: 18px;
accent-color: var(--pokemon-blue);
}
@media (max-width: 720px) { @media (max-width: 720px) {
.item-edit-row--name-price, .item-edit-row--name-price,
.item-edit-row--category-usage { .item-edit-row--category-usage {

View File

@@ -48,11 +48,12 @@ const suppressNextItemClick = ref(false);
const dragSourceItems = ref<Item[]>([]); const dragSourceItems = ref<Item[]>([]);
const dropCommitted = ref(false); const dropCommitted = ref(false);
type Dyeability = 0 | 1 | 2 | 3;
type ItemCreateDefaults = { type ItemCreateDefaults = {
categoryId: string; categoryId: string;
usageId: string; usageId: string;
dyeable: boolean; dyeability: Dyeability;
dualDyeable: boolean;
patternEditable: boolean; patternEditable: boolean;
noRecipe: boolean; noRecipe: boolean;
acquisitionMethodIds: string[]; acquisitionMethodIds: string[];
@@ -63,8 +64,7 @@ const itemCreateDefaultsStorageKey = 'pokopia_item_create_defaults';
const emptyItemCreateDefaults = (): ItemCreateDefaults => ({ const emptyItemCreateDefaults = (): ItemCreateDefaults => ({
categoryId: '', categoryId: '',
usageId: '', usageId: '',
dyeable: false, dyeability: 0,
dualDyeable: false,
patternEditable: false, patternEditable: false,
noRecipe: false, noRecipe: false,
acquisitionMethodIds: [] acquisitionMethodIds: []
@@ -96,6 +96,12 @@ const categoryTabs = computed<TabOption[]>(() => [
{ value: '', label: t('common.all') }, { value: '', label: t('common.all') },
...(options.value?.itemCategories.map((item) => ({ value: String(item.id), label: item.name })) ?? []) ...(options.value?.itemCategories.map((item) => ({ value: String(item.id), label: item.name })) ?? [])
]); ]);
const dyeabilityOptions = computed<Array<{ value: Dyeability; label: string }>>(() => [
{ value: 0, label: t('pages.items.notDyeable') },
{ value: 1, label: t('pages.items.dyeable') },
{ value: 2, label: t('pages.items.dualDyeable') },
{ value: 3, label: t('pages.items.tripleDyeable') }
]);
const itemQuery = computed(() => ({ const itemQuery = computed(() => ({
search: search.value, search: search.value,
@@ -156,8 +162,7 @@ const hasItemCreateDefaults = computed(
() => () =>
itemCreateDefaults.value.categoryId !== '' || itemCreateDefaults.value.categoryId !== '' ||
itemCreateDefaults.value.usageId !== '' || itemCreateDefaults.value.usageId !== '' ||
itemCreateDefaults.value.dyeable || itemCreateDefaults.value.dyeability !== 0 ||
itemCreateDefaults.value.dualDyeable ||
itemCreateDefaults.value.patternEditable || itemCreateDefaults.value.patternEditable ||
itemCreateDefaults.value.noRecipe || itemCreateDefaults.value.noRecipe ||
itemCreateDefaults.value.acquisitionMethodIds.length > 0 itemCreateDefaults.value.acquisitionMethodIds.length > 0
@@ -218,6 +223,20 @@ function menuPositionForEvent(event: MouseEvent | KeyboardEvent) {
return clampMenuPosition(window.innerWidth / 2, window.innerHeight / 2); return clampMenuPosition(window.innerWidth / 2, window.innerHeight / 2);
} }
function defaultDyeability(value: { dyeability?: unknown; dualDyeable?: unknown; dyeable?: unknown }): Dyeability {
const dyeability = Number(value.dyeability);
if (Number.isInteger(dyeability) && dyeability >= 0 && dyeability <= 3) {
return dyeability as Dyeability;
}
if (value.dualDyeable === true) {
return 2;
}
if (value.dyeable === true) {
return 1;
}
return 0;
}
function readItemCreateDefaults(): ItemCreateDefaults { function readItemCreateDefaults(): ItemCreateDefaults {
if (typeof sessionStorage === 'undefined') { if (typeof sessionStorage === 'undefined') {
return emptyItemCreateDefaults(); return emptyItemCreateDefaults();
@@ -233,8 +252,7 @@ function readItemCreateDefaults(): ItemCreateDefaults {
return { return {
categoryId: typeof parsedValue.categoryId === 'string' ? parsedValue.categoryId : '', categoryId: typeof parsedValue.categoryId === 'string' ? parsedValue.categoryId : '',
usageId: typeof parsedValue.usageId === 'string' ? parsedValue.usageId : '', usageId: typeof parsedValue.usageId === 'string' ? parsedValue.usageId : '',
dyeable: parsedValue.dyeable === true, dyeability: defaultDyeability(parsedValue),
dualDyeable: parsedValue.dualDyeable === true,
patternEditable: parsedValue.patternEditable === true, patternEditable: parsedValue.patternEditable === true,
noRecipe: parsedValue.noRecipe === true, noRecipe: parsedValue.noRecipe === true,
acquisitionMethodIds: Array.isArray(parsedValue.acquisitionMethodIds) acquisitionMethodIds: Array.isArray(parsedValue.acquisitionMethodIds)
@@ -638,9 +656,17 @@ watch(itemSortingAllowed, (allowed) => {
/> />
</div> </div>
<fieldset class="radio-group">
<legend>{{ t('pages.items.dyeability') }}</legend>
<div class="radio-group__options">
<label v-for="option in dyeabilityOptions" :key="option.value" class="radio-group__option">
<input v-model="itemCreateDefaults.dyeability" type="radio" name="item-default-dyeability" :value="option.value" />
<span>{{ option.label }}</span>
</label>
</div>
</fieldset>
<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.dualDyeable" type="checkbox" /> {{ t('pages.items.dualDyeable') }}</label>
<label><input v-model="itemCreateDefaults.patternEditable" type="checkbox" /> {{ t('pages.items.patternEditable') }}</label> <label><input v-model="itemCreateDefaults.patternEditable" type="checkbox" /> {{ t('pages.items.patternEditable') }}</label>
<label><input v-model="itemCreateDefaults.noRecipe" type="checkbox" /> {{ t('pages.items.noRecipe') }}</label> <label><input v-model="itemCreateDefaults.noRecipe" type="checkbox" /> {{ t('pages.items.noRecipe') }}</label>
</div> </div>

View File

@@ -708,8 +708,11 @@ export const systemWordingMessages = {
tags: 'Tags', tags: 'Tags',
acquisitionMethods: 'Acquisition methods', acquisitionMethods: 'Acquisition methods',
customization: 'Customization', customization: 'Customization',
dyeability: 'Dyeability',
notDyeable: 'Not dyeable',
dyeable: 'Dyeable', dyeable: 'Dyeable',
dualDyeable: 'Dual dyeable', dualDyeable: 'Dual dyeable',
tripleDyeable: 'Triple dyeable',
patternEditable: 'Pattern editable', patternEditable: 'Pattern editable',
noRecipe: 'No recipe', noRecipe: 'No recipe',
eventItem: 'Event item', eventItem: 'Event item',
@@ -2075,8 +2078,11 @@ export const systemWordingMessages = {
tags: '标签', tags: '标签',
acquisitionMethods: '入手方式', acquisitionMethods: '入手方式',
customization: '自定义', customization: '自定义',
dyeability: '染色能力',
notDyeable: '不可染色',
dyeable: '可染色', dyeable: '可染色',
dualDyeable: '可双区染色', dualDyeable: '可双区染色',
tripleDyeable: '可三区染色',
patternEditable: '可改花纹', patternEditable: '可改花纹',
noRecipe: '无材料单', noRecipe: '无材料单',
eventItem: '活动物品', eventItem: '活动物品',