feat(pokemon): redesign image display with side thumbnail and modal
Move image thumbnail to the right of base stats Display detailed image information in a modal
This commit is contained in:
@@ -277,11 +277,11 @@ Pokemon 列表功能:
|
||||
Pokemon 详情页展示:
|
||||
|
||||
- 基本信息
|
||||
- 已配置图片时,详情主内容顶部展示 Pokédex 风格图片区,包含大图和图片版本说明;未配置图片时不显示图片区。
|
||||
- 已配置图片时,详情主内容在六维 Stats 右侧展示正方形居中的 Pokédex 风格图片;页面内不直接展示图片版本、状态或描述,用户可通过图片 Modal 查看详情;未配置图片时不显示图片区。
|
||||
- 主内容顶部按以下布局展示:
|
||||
- 左上:Genus & Details;无区块标题;如有 Genus,先展示 Genus,再以分割线连接 Details 内容
|
||||
- 左下:Height / Weight 与 Types 按 2:1 比例并排;Height / Weight 无区块标题,在 Dimension 区内左右并排展示并以中间分割线隔开,每组按英制、分割线、公制、标签上下排列;Types 不显示 Type 1 / Type 2 文案,上下布局并居中展示
|
||||
- 右侧:六维 Stats
|
||||
- 右侧:六维 Stats;已配置图片时图片展示在 Stats 右侧
|
||||
- 六维使用 ProgressBar 展示,最大值按 150 计算。
|
||||
- 特长
|
||||
- 特长掉落物品
|
||||
|
||||
@@ -3379,6 +3379,21 @@ button:disabled,
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.pokemon-profile-grid--with-image {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(430px, 560px);
|
||||
}
|
||||
|
||||
.pokemon-profile-side {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
align-items: stretch;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pokemon-profile-side--with-image {
|
||||
grid-template-columns: minmax(0, 1fr) clamp(112px, 12vw, 164px);
|
||||
}
|
||||
|
||||
.pokemon-profile-main,
|
||||
.pokemon-profile-row {
|
||||
display: grid;
|
||||
@@ -3400,6 +3415,36 @@ button:disabled,
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.pokemon-profile-image {
|
||||
width: clamp(112px, 12vw, 164px);
|
||||
aspect-ratio: 1 / 1;
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 10px;
|
||||
border: 4px solid #172036;
|
||||
border-radius: var(--radius-card);
|
||||
background:
|
||||
linear-gradient(90deg, rgba(42, 117, 187, 0.08) 1px, transparent 1px) 0 0 / 18px 18px,
|
||||
linear-gradient(rgba(42, 117, 187, 0.08) 1px, transparent 1px) 0 0 / 18px 18px,
|
||||
#eef9ff;
|
||||
box-shadow: var(--shadow-soft);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pokemon-profile-image:hover,
|
||||
.pokemon-profile-image:focus-visible {
|
||||
border-color: var(--pokemon-blue);
|
||||
}
|
||||
|
||||
.pokemon-profile-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
.pokemon-types-card {
|
||||
align-content: center;
|
||||
justify-items: center;
|
||||
@@ -4137,6 +4182,16 @@ button:disabled,
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.pokemon-profile-side--with-image {
|
||||
grid-template-columns: minmax(0, 1fr) clamp(96px, 21vw, 132px);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.pokemon-profile-image {
|
||||
width: clamp(96px, 21vw, 132px);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.coming-soon-panel {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 18px;
|
||||
|
||||
@@ -7,6 +7,7 @@ import DetailSection from '../components/DetailSection.vue';
|
||||
import EditHistoryPanel from '../components/EditHistoryPanel.vue';
|
||||
import EntityDiscussionPanel from '../components/EntityDiscussionPanel.vue';
|
||||
import EntityChips from '../components/EntityChips.vue';
|
||||
import Modal from '../components/Modal.vue';
|
||||
import PageHeader from '../components/PageHeader.vue';
|
||||
import PokemonStatsPanel from '../components/PokemonStatsPanel.vue';
|
||||
import Skeleton from '../components/Skeleton.vue';
|
||||
@@ -21,6 +22,7 @@ const pokemon = ref<PokemonDetail | null>(null);
|
||||
const itemCategoryTab = ref('');
|
||||
const relatedHabitatTab = ref('');
|
||||
const detailTab = ref('details');
|
||||
const imageModalOpen = ref(false);
|
||||
const timeOfDays = ['早晨', '中午', '傍晚', '晚上'];
|
||||
const weathers = ['晴天', '阴天', '雨天'];
|
||||
const relatedPokemonLimit = 6;
|
||||
@@ -192,6 +194,14 @@ function pokemonImageLabel() {
|
||||
return pokemon.value?.image ? `${pokemon.value.image.version} - ${pokemon.value.image.variant}` : '';
|
||||
}
|
||||
|
||||
function openImageModal() {
|
||||
imageModalOpen.value = true;
|
||||
}
|
||||
|
||||
function closeImageModal() {
|
||||
imageModalOpen.value = false;
|
||||
}
|
||||
|
||||
async function loadPokemonDetail() {
|
||||
const nextPokemon = await api.pokemonDetail(String(route.params.id));
|
||||
pokemon.value = nextPokemon;
|
||||
@@ -217,6 +227,7 @@ watch(
|
||||
pokemon.value = null;
|
||||
relatedHabitatTab.value = '';
|
||||
detailTab.value = 'details';
|
||||
imageModalOpen.value = false;
|
||||
void loadPokemonDetail();
|
||||
}
|
||||
);
|
||||
@@ -298,18 +309,7 @@ watch(
|
||||
<Tabs id="pokemon-detail-tabs" v-model="detailTab" :tabs="detailTabs" :label="t('common.details')" />
|
||||
|
||||
<div v-if="detailTab === 'details'" class="detail-grid detail-grid--stack">
|
||||
<section v-if="pokemon.image" class="detail-section pokemon-image-detail" :aria-label="t('pages.pokemon.image')">
|
||||
<div class="pokemon-image-detail__screen">
|
||||
<img :src="pokemon.image.url" :alt="pokemonImageAlt()" />
|
||||
</div>
|
||||
<div class="pokemon-image-detail__caption">
|
||||
<strong>{{ pokemonImageLabel() }}</strong>
|
||||
<span>{{ pokemon.image.style }}</span>
|
||||
<p>{{ pokemon.image.description }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="pokemon-profile-grid">
|
||||
<div class="pokemon-profile-grid" :class="{ 'pokemon-profile-grid--with-image': pokemon.image }">
|
||||
<div class="pokemon-profile-main">
|
||||
<section class="detail-section pokemon-profile-card" :aria-label="t('pages.pokemon.details')">
|
||||
<p v-if="pokemon.genus" class="pokemon-genus">{{ pokemon.genus }}</p>
|
||||
@@ -352,9 +352,15 @@ watch(
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DetailSection class="pokemon-profile-stats" :title="t('pages.pokemon.statsTitle')">
|
||||
<PokemonStatsPanel :stats="pokemon.stats" />
|
||||
</DetailSection>
|
||||
<div class="pokemon-profile-side" :class="{ 'pokemon-profile-side--with-image': pokemon.image }">
|
||||
<DetailSection class="pokemon-profile-stats" :title="t('pages.pokemon.statsTitle')">
|
||||
<PokemonStatsPanel :stats="pokemon.stats" />
|
||||
</DetailSection>
|
||||
|
||||
<button v-if="pokemon.image" type="button" class="pokemon-profile-image" :aria-label="pokemonImageLabel()" @click="openImageModal">
|
||||
<img :src="pokemon.image.url" :alt="pokemonImageAlt()" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DetailSection :title="t('pages.pokemon.skills')">
|
||||
@@ -482,5 +488,25 @@ watch(
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Modal
|
||||
v-if="pokemon?.image && imageModalOpen"
|
||||
:title="t('pages.pokemon.image')"
|
||||
:subtitle="pokemonImageLabel()"
|
||||
:close-label="t('common.close')"
|
||||
size="wide"
|
||||
@close="closeImageModal"
|
||||
>
|
||||
<div class="pokemon-image-detail">
|
||||
<div class="pokemon-image-detail__screen">
|
||||
<img :src="pokemon.image.url" :alt="pokemonImageAlt()" />
|
||||
</div>
|
||||
<div class="pokemon-image-detail__caption">
|
||||
<strong>{{ pokemonImageLabel() }}</strong>
|
||||
<span>{{ pokemon.image.style }}</span>
|
||||
<p>{{ pokemon.image.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<PokemonEdit v-if="showEditor" />
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user