feat(ui): implement compact grid layout for items and artifacts

Add compact tooltip mode to EntityCard component
Display 12-column icon grid on desktop for collections
Retain standard card layout with details on mobile devices
This commit is contained in:
2026-05-04 22:19:36 +08:00
parent 28f4e6032c
commit cd0f8868c3
5 changed files with 168 additions and 15 deletions

View File

@@ -638,8 +638,9 @@ Items 与 Event Items 使用相同数据模型:
- 按用途筛选 - 按用途筛选
- 按标签筛选 - 按标签筛选
- 按自定义排序展示 - 按自定义排序展示
- 物品列表卡片使用与 Pokemon 列表一致的居中图鉴式布局,只展示物品图标、名称和分类;不展示标签、入手方式或编辑元信息 - 物品列表桌面端使用 12 列紧凑 Grid每个格子只展示物品图标有用途的物品在卡片左上角以斜 Ribbon 展示用途名称;物品名称通过 hover / focus Tooltip 展示
- 有用途的物品在卡片左上角以斜 Ribbon 展示用途名称 - 物品列表移动端保持常规卡片布局,展示物品图标、名称和分类
- 物品列表不展示标签、入手方式或编辑元信息。
- 已配置图标时,物品卡片展示图标缩略图;未配置图标时保留默认物品标记。 - 已配置图标时,物品卡片展示图标缩略图;未配置图标时保留默认物品标记。
物品详情页展示: 物品详情页展示:
@@ -682,7 +683,9 @@ Ancient Artifacts 列表功能:
- 按分类展示为标签页 - 按分类展示为标签页
- 按标签筛选 - 按标签筛选
- 按自定义排序展示 - 按自定义排序展示
- 列表卡片使用与 Pokemon 列表一致的居中图鉴式布局,展示图片 / 默认 Ancient Artifact 标记名称和分类;不展示编辑元信息 - 列表桌面端使用 12 列紧凑 Grid每个格子只展示图片 / 默认 Ancient Artifact 标记名称通过 hover / focus Tooltip 展示
- 列表移动端保持常规卡片布局,展示图片 / 默认 Ancient Artifact 标记、名称和分类。
- 列表不展示编辑元信息。
Ancient Artifacts 详情页展示: Ancient Artifacts 详情页展示:

View File

@@ -11,18 +11,28 @@ defineProps<{
marker?: string; marker?: string;
image?: { src: string; alt: string }; image?: { src: string; alt: string };
ribbon?: string; ribbon?: string;
compactTooltip?: boolean;
}>(); }>();
</script> </script>
<template> <template>
<RouterLink v-if="to" class="entity-card entity-card--link" :to="to"> <RouterLink
<span v-if="ribbon" class="entity-card__ribbon">{{ ribbon }}</span> v-if="to"
class="entity-card entity-card--link"
:class="{ 'entity-card--collection-compact': compactTooltip }"
:to="to"
:aria-label="compactTooltip ? title : undefined"
>
<span v-if="ribbon" class="entity-card__ribbon-clip" aria-hidden="true">
<span class="entity-card__ribbon">{{ ribbon }}</span>
</span>
<span class="entity-card__mark" :class="{ 'entity-card__mark--image': image }"> <span class="entity-card__mark" :class="{ 'entity-card__mark--image': image }">
<img v-if="image" class="entity-card__image" :src="image.src" :alt="image.alt" loading="lazy" /> <img v-if="image" class="entity-card__image" :src="image.src" :alt="image.alt" loading="lazy" />
<Icon v-else-if="icon" :icon="icon" class="entity-card__icon" aria-hidden="true" /> <Icon v-else-if="icon" :icon="icon" class="entity-card__icon" aria-hidden="true" />
<PokeBallMark v-else-if="!marker" size="30px" /> <PokeBallMark v-else-if="!marker" size="30px" />
<span v-else>{{ marker }}</span> <span v-else>{{ marker }}</span>
</span> </span>
<span v-if="compactTooltip" class="entity-card__tooltip" role="tooltip">{{ title }}</span>
<div class="entity-card__content"> <div class="entity-card__content">
<span class="entity-card__title">{{ title }}</span> <span class="entity-card__title">{{ title }}</span>
<slot name="after-title"></slot> <slot name="after-title"></slot>
@@ -31,14 +41,17 @@ defineProps<{
</div> </div>
</RouterLink> </RouterLink>
<article v-else class="entity-card"> <article v-else class="entity-card" :class="{ 'entity-card--collection-compact': compactTooltip }">
<span v-if="ribbon" class="entity-card__ribbon">{{ ribbon }}</span> <span v-if="ribbon" class="entity-card__ribbon-clip" aria-hidden="true">
<span class="entity-card__ribbon">{{ ribbon }}</span>
</span>
<span class="entity-card__mark" :class="{ 'entity-card__mark--image': image }"> <span class="entity-card__mark" :class="{ 'entity-card__mark--image': image }">
<img v-if="image" class="entity-card__image" :src="image.src" :alt="image.alt" loading="lazy" /> <img v-if="image" class="entity-card__image" :src="image.src" :alt="image.alt" loading="lazy" />
<Icon v-else-if="icon" :icon="icon" class="entity-card__icon" aria-hidden="true" /> <Icon v-else-if="icon" :icon="icon" class="entity-card__icon" aria-hidden="true" />
<PokeBallMark v-else-if="!marker" size="30px" /> <PokeBallMark v-else-if="!marker" size="30px" />
<span v-else>{{ marker }}</span> <span v-else>{{ marker }}</span>
</span> </span>
<span v-if="compactTooltip" class="entity-card__tooltip" role="tooltip">{{ title }}</span>
<div class="entity-card__content"> <div class="entity-card__content">
<span class="entity-card__title">{{ title }}</span> <span class="entity-card__title">{{ title }}</span>
<slot name="after-title"></slot> <slot name="after-title"></slot>

View File

@@ -2343,9 +2343,17 @@ button:disabled,
object-fit: contain; object-fit: contain;
} }
.entity-card__ribbon { .entity-card__ribbon-clip {
position: absolute; position: absolute;
z-index: 1; z-index: 1;
inset: 0;
overflow: hidden;
border-radius: calc(var(--radius-card) - 2px);
pointer-events: none;
}
.entity-card__ribbon {
position: absolute;
top: 14px; top: 14px;
left: -38px; left: -38px;
width: 132px; width: 132px;
@@ -2362,7 +2370,6 @@ button:disabled,
font-size: 0.72rem; font-size: 0.72rem;
font-weight: 950; font-weight: 950;
line-height: 1; line-height: 1;
pointer-events: none;
text-align: center; text-align: center;
} }
@@ -2438,6 +2445,86 @@ button:disabled,
font-weight: 850; font-weight: 850;
} }
.collections-card-grid {
grid-template-columns: repeat(12, minmax(0, 1fr));
gap: 10px;
}
.collections-card-grid .entity-card--collection-compact {
min-height: 0;
aspect-ratio: 1;
align-content: center;
justify-content: center;
gap: 0;
padding: 10px;
overflow: visible;
}
.collections-card-grid .entity-card--collection-compact:hover,
.collections-card-grid .entity-card--collection-compact:focus-visible {
z-index: 4;
}
.collections-card-grid .entity-card--collection-compact .entity-card__mark {
width: min(100%, 72px);
height: auto;
aspect-ratio: 1;
}
.collections-card-grid .entity-card--collection-compact .skeleton-entity-mark {
width: min(100%, 72px) !important;
height: auto !important;
aspect-ratio: 1;
}
.collections-card-grid .entity-card--collection-compact .entity-card__content {
display: none;
}
.entity-card__tooltip {
position: absolute;
z-index: 5;
bottom: calc(100% + 8px);
left: 50%;
width: max-content;
max-width: 180px;
padding: 6px 8px;
transform: translate(-50%, 4px);
border: 2px solid var(--line-strong);
border-radius: var(--radius-small);
background: var(--surface-raised);
color: var(--ink);
box-shadow: 0 3px 0 var(--line-strong);
font-size: 0.82rem;
font-weight: 850;
line-height: 1.25;
opacity: 0;
pointer-events: none;
text-align: center;
transition:
opacity 0.14s ease,
transform 0.14s ease;
}
.entity-card__tooltip::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
width: 10px;
height: 10px;
transform: translate(-50%, -4px) rotate(45deg);
border-right: 2px solid var(--line-strong);
border-bottom: 2px solid var(--line-strong);
background: var(--surface-raised);
}
.entity-card--collection-compact:hover .entity-card__tooltip,
.entity-card--collection-compact:focus-visible .entity-card__tooltip {
transform: translate(-50%, 0);
opacity: 1;
}
.catalog-card-action { .catalog-card-action {
min-height: 36px; min-height: 36px;
max-width: 100%; max-width: 100%;
@@ -7759,6 +7846,31 @@ button:disabled,
text-align: left; text-align: left;
} }
.collections-card-grid {
gap: 10px;
}
.collections-card-grid .entity-card--collection-compact {
aspect-ratio: auto;
justify-content: stretch;
gap: 10px;
padding: 12px;
overflow: hidden;
}
.collections-card-grid .entity-card--collection-compact .entity-card__content {
display: grid;
}
.collections-card-grid .entity-card--collection-compact .skeleton-entity-mark {
width: 56px !important;
height: 56px !important;
}
.collections-card-grid .entity-card--collection-compact .entity-card__tooltip {
display: none;
}
.pokemon-list-grid .entity-card__mark, .pokemon-list-grid .entity-card__mark,
.catalog-card-grid .entity-card__mark { .catalog-card-grid .entity-card__mark {
width: 56px; width: 56px;
@@ -8689,6 +8801,11 @@ button:disabled,
padding: 10px; padding: 10px;
} }
.collections-card-grid .entity-card--collection-compact {
gap: 8px;
padding: 10px;
}
.entity-card__mark { .entity-card__mark {
width: 34px; width: 34px;
height: 34px; height: 34px;
@@ -8700,6 +8817,16 @@ button:disabled,
height: 48px; height: 48px;
} }
.collections-card-grid .entity-card--collection-compact .entity-card__mark {
width: 48px;
height: 48px;
}
.collections-card-grid .entity-card--collection-compact .skeleton-entity-mark {
width: 48px !important;
height: 48px !important;
}
.pokemon-list-grid .pokeball-mark, .pokemon-list-grid .pokeball-mark,
.catalog-card-grid .pokeball-mark { .catalog-card-grid .pokeball-mark {
--ball-size: 36px !important; --ball-size: 36px !important;

View File

@@ -113,8 +113,12 @@ watch(artifactQuery, loadArtifacts);
</div> </div>
</FilterPanel> </FilterPanel>
<div v-if="loading" class="entity-grid catalog-card-grid" aria-busy="true" :aria-label="t('pages.ancientArtifacts.loadingList')"> <div v-if="loading" class="entity-grid catalog-card-grid collections-card-grid" aria-busy="true" :aria-label="t('pages.ancientArtifacts.loadingList')">
<article v-for="index in skeletonCardCount" :key="`artifact-skeleton-${index}`" class="entity-card entity-card--skeleton"> <article
v-for="index in skeletonCardCount"
:key="`artifact-skeleton-${index}`"
class="entity-card entity-card--skeleton entity-card--collection-compact"
>
<Skeleton variant="box" width="92px" height="92px" class="skeleton-entity-mark" /> <Skeleton variant="box" width="92px" height="92px" class="skeleton-entity-mark" />
<div class="entity-card__content"> <div class="entity-card__content">
<Skeleton width="128px" height="24px" /> <Skeleton width="128px" height="24px" />
@@ -122,7 +126,7 @@ watch(artifactQuery, loadArtifacts);
</div> </div>
</article> </article>
</div> </div>
<div v-else class="entity-grid catalog-card-grid"> <div v-else class="entity-grid catalog-card-grid collections-card-grid">
<EntityCard <EntityCard
v-for="artifact in artifacts" v-for="artifact in artifacts"
:key="artifact.id" :key="artifact.id"
@@ -131,6 +135,7 @@ watch(artifactQuery, loadArtifacts);
:to="`/ancient-artifacts/${artifact.id}`" :to="`/ancient-artifacts/${artifact.id}`"
:icon="iconArtifact" :icon="iconArtifact"
:image="artifactCardImage(artifact)" :image="artifactCardImage(artifact)"
compact-tooltip
/> />
</div> </div>

View File

@@ -132,8 +132,12 @@ watch(itemQuery, loadItems);
</div> </div>
</FilterPanel> </FilterPanel>
<div v-if="loading" class="entity-grid catalog-card-grid" aria-busy="true" :aria-label="t('pages.items.loadingList')"> <div v-if="loading" class="entity-grid catalog-card-grid collections-card-grid" aria-busy="true" :aria-label="t('pages.items.loadingList')">
<article v-for="index in skeletonCardCount" :key="`item-skeleton-${index}`" class="entity-card entity-card--skeleton"> <article
v-for="index in skeletonCardCount"
:key="`item-skeleton-${index}`"
class="entity-card entity-card--skeleton entity-card--collection-compact"
>
<Skeleton variant="box" width="92px" height="92px" class="skeleton-entity-mark" /> <Skeleton variant="box" width="92px" height="92px" class="skeleton-entity-mark" />
<div class="entity-card__content"> <div class="entity-card__content">
<Skeleton width="128px" height="24px" /> <Skeleton width="128px" height="24px" />
@@ -141,7 +145,7 @@ watch(itemQuery, loadItems);
</div> </div>
</article> </article>
</div> </div>
<div v-else class="entity-grid catalog-card-grid"> <div v-else class="entity-grid catalog-card-grid collections-card-grid">
<EntityCard <EntityCard
v-for="item in items" v-for="item in items"
:key="item.id" :key="item.id"
@@ -151,6 +155,7 @@ watch(itemQuery, loadItems);
:icon="iconItem" :icon="iconItem"
:image="itemCardImage(item)" :image="itemCardImage(item)"
:ribbon="item.usage?.name" :ribbon="item.usage?.name"
compact-tooltip
/> />
</div> </div>