From a17344d216000e853a7103cc10ccf8c8fd3171b8 Mon Sep 17 00:00:00 2001 From: xiaomai Date: Mon, 4 May 2026 22:45:32 +0800 Subject: [PATCH] feat(ui): add session defaults menu for item creation Support presetting category, checkboxes, and acquisition methods. Persist defaults in sessionStorage to streamline repetitive data entry. --- DESIGN.md | 1 + frontend/src/styles/main.css | 90 +++++++++++++ frontend/src/views/ItemEdit.vue | 85 +++++++++++- frontend/src/views/ItemsList.vue | 217 ++++++++++++++++++++++++++++++- system-wordings.ts | 8 ++ 5 files changed, 392 insertions(+), 9 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index 034c7ad..16d19e7 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -638,6 +638,7 @@ Items 与 Event Items 使用相同数据模型: - 按用途筛选 - 按标签筛选 - 按自定义排序展示 +- 新增物品入口支持当前浏览器 Session 的默认值菜单;用户可为新建物品预设分类、客制化勾选项和入手方式。默认值只影响 `/items/new` 与 `/event-items/new` 的新建表单初始值,不影响编辑已有物品,不改变 API、数据库模型、权限或审计行为;Event Items 仍由 `/event-items/new` 入口决定 `is_event_item`。 - 物品列表桌面端使用 12 列紧凑 Grid,每个格子只展示物品图标;有用途的物品在卡片左上角以斜 Ribbon 展示用途名称;物品名称通过 hover / focus Tooltip 展示。 - 物品列表移动端保持常规卡片布局,展示物品图标、名称和分类。 - 物品列表不展示标签、入手方式或编辑元信息。 diff --git a/frontend/src/styles/main.css b/frontend/src/styles/main.css index be61368..e7b730f 100644 --- a/frontend/src/styles/main.css +++ b/frontend/src/styles/main.css @@ -1346,6 +1346,96 @@ button:disabled, box-shadow: 0 2px 0 var(--line); } +.item-create-action { + position: relative; + display: inline-flex; + align-items: flex-start; +} + +.item-create-action__control { + display: inline-flex; + align-items: stretch; + border-radius: var(--radius-control); + box-shadow: 0 2px 0 var(--line-strong); +} + +.item-create-action__control .ui-button { + box-shadow: none; +} + +.item-create-action__control .ui-button:hover, +.item-create-action__control .ui-button:active { + transform: none; + box-shadow: none; +} + +.item-create-action__control .ui-button:disabled { + box-shadow: none; +} + +.item-create-action__primary { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.item-create-action__menu-button { + position: relative; + min-width: 38px; + padding-inline: 8px; + border-left-width: 1px; + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.item-create-action__control.has-defaults .item-create-action__menu-button::after { + content: ''; + position: absolute; + top: 6px; + right: 6px; + width: 7px; + height: 7px; + border: 1px solid var(--line-strong); + border-radius: 50%; + background: var(--pokemon-blue); +} + +.item-create-defaults-menu { + position: absolute; + top: calc(100% + 8px); + right: 0; + z-index: 45; + width: min(360px, calc(100vw - 32px)); + display: grid; + gap: 14px; + padding: 12px; + border: 2px solid var(--line-strong); + border-radius: var(--radius-card); + background: var(--surface); + box-shadow: var(--shadow-raised); +} + +.item-create-defaults-menu__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.item-create-defaults-menu__header strong { + color: var(--ink); + font-size: 14px; + font-weight: 900; +} + +.item-create-defaults-menu .field { + min-width: 0; +} + +.item-create-defaults-menu__checks { + display: grid; + gap: 8px; +} + .filter-panel, .toolbar { display: grid; diff --git a/frontend/src/views/ItemEdit.vue b/frontend/src/views/ItemEdit.vue index 1664e72..c5e451e 100644 --- a/frontend/src/views/ItemEdit.vue +++ b/frontend/src/views/ItemEdit.vue @@ -51,6 +51,17 @@ const itemForm = ref({ imagePath: '' }); +type ItemCreateDefaults = { + categoryId: string; + dyeable: boolean; + dualDyeable: boolean; + patternEditable: boolean; + noRecipe: boolean; + acquisitionMethodIds: string[]; +}; + +const itemCreateDefaultsStorageKey = 'pokopia_item_create_defaults'; + const routeId = computed(() => (typeof route.params.id === 'string' ? route.params.id : '')); const isEditing = computed(() => routeId.value !== ''); const isEventCreate = computed(() => route.name === 'event-item-new'); @@ -75,6 +86,76 @@ function errorText(error: unknown, fallback: string) { return error instanceof Error && error.message ? error.message : fallback; } +function readItemCreateDefaults(): ItemCreateDefaults { + if (typeof sessionStorage === 'undefined') { + return { + categoryId: '', + dyeable: false, + dualDyeable: false, + patternEditable: false, + noRecipe: false, + acquisitionMethodIds: [] + }; + } + + try { + const rawValue = sessionStorage.getItem(itemCreateDefaultsStorageKey); + if (!rawValue) { + return { + categoryId: '', + dyeable: false, + dualDyeable: false, + patternEditable: false, + noRecipe: false, + acquisitionMethodIds: [] + }; + } + + const parsedValue = JSON.parse(rawValue) as Partial; + return { + categoryId: typeof parsedValue.categoryId === 'string' ? parsedValue.categoryId : '', + dyeable: parsedValue.dyeable === true, + dualDyeable: parsedValue.dualDyeable === true, + patternEditable: parsedValue.patternEditable === true, + noRecipe: parsedValue.noRecipe === true, + acquisitionMethodIds: Array.isArray(parsedValue.acquisitionMethodIds) + ? parsedValue.acquisitionMethodIds.filter((item) => typeof item === 'string') + : [] + }; + } catch { + return { + categoryId: '', + dyeable: false, + dualDyeable: false, + patternEditable: false, + noRecipe: false, + acquisitionMethodIds: [] + }; + } +} + +function applyItemCreateDefaults(isEventItem: boolean) { + const loadedOptions = options.value; + if (!loadedOptions) { + itemForm.value.isEventItem = isEventItem; + return; + } + + const defaults = readItemCreateDefaults(); + const categoryIds = new Set(loadedOptions.itemCategories.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 : '', + dyeable: defaults.dyeable, + dualDyeable: defaults.dualDyeable, + patternEditable: defaults.patternEditable, + noRecipe: defaults.noRecipe, + isEventItem, + acquisitionMethodIds: defaults.acquisitionMethodIds.filter((item) => methodIds.has(item)) + }; +} + function closeEditor() { void router.push(cancelTo.value); } @@ -133,10 +214,8 @@ async function loadEditor() { currentImage.value = item.image; imageHistory.value = item.imageHistory; hasRecipe.value = item.recipe !== null; - } else if (isEventCreate.value) { - itemForm.value.isEventItem = true; } else { - itemForm.value.isEventItem = false; + applyItemCreateDefaults(isEventCreate.value); } } catch (error) { message.value = errorText(error, t('errors.loadFailed')); diff --git a/frontend/src/views/ItemsList.vue b/frontend/src/views/ItemsList.vue index 5eddf6a..a93cc44 100644 --- a/frontend/src/views/ItemsList.vue +++ b/frontend/src/views/ItemsList.vue @@ -1,6 +1,6 @@