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:
@@ -645,8 +645,11 @@ Pokemon 详情页展示:
|
|||||||
- Road
|
- Road
|
||||||
- 入手方式:可多选
|
- 入手方式:可多选
|
||||||
- 客制化:
|
- 客制化:
|
||||||
- 可染色
|
- 染色能力:`dyeability`,使用互斥枚举值维护:
|
||||||
- 可双区染色
|
- `0`:不可染色
|
||||||
|
- `1`:可染色
|
||||||
|
- `2`:可双区染色
|
||||||
|
- `3`:可三区染色
|
||||||
- 可改花纹
|
- 可改花纹
|
||||||
- 无材料单:`no_recipe`
|
- 无材料单:`no_recipe`
|
||||||
- 标签:使用喜欢的东西配置,可多选
|
- 标签:使用喜欢的东西配置,可多选
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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: '活动物品',
|
||||||
|
|||||||
Reference in New Issue
Block a user