feat(ui): aggregate appearance data in detail views
Group appearances by entity ID and rarity to reduce list clutter Display aggregated times, weathers, and maps in a structured layout
This commit is contained in:
@@ -390,6 +390,42 @@ select {
|
|||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.appearance-list li {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: max-content minmax(0, 1fr);
|
||||||
|
align-items: start;
|
||||||
|
justify-content: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appearance-name {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appearance-summary {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
color: #657067;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appearance-summary div {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 72px minmax(0, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appearance-summary dt,
|
||||||
|
.appearance-summary dd {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appearance-summary dt {
|
||||||
|
color: #566156;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
.link-button {
|
.link-button {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -527,4 +563,13 @@ button:disabled {
|
|||||||
.appearance-row {
|
.appearance-row {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.appearance-list li {
|
||||||
|
align-items: start;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appearance-list {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,75 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import EntityChips from '../components/EntityChips.vue';
|
import EntityChips from '../components/EntityChips.vue';
|
||||||
import { api, type HabitatDetail } from '../services/api';
|
import { api, type HabitatDetail } from '../services/api';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const habitat = ref<HabitatDetail | null>(null);
|
const habitat = ref<HabitatDetail | null>(null);
|
||||||
|
const timeOfDays = ['早晨', '中午', '傍晚', '晚上'];
|
||||||
|
const weathers = ['晴天', '阴天', '雨天'];
|
||||||
|
|
||||||
|
type PokemonRow = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
timeOfDays: string[];
|
||||||
|
weathers: string[];
|
||||||
|
rarity: number;
|
||||||
|
maps: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function sortByOrder(values: Set<string>, order: string[]) {
|
||||||
|
return [...values].sort((a, b) => {
|
||||||
|
const indexA = order.indexOf(a);
|
||||||
|
const indexB = order.indexOf(b);
|
||||||
|
if (indexA === -1 && indexB === -1) return a.localeCompare(b);
|
||||||
|
if (indexA === -1) return 1;
|
||||||
|
if (indexB === -1) return -1;
|
||||||
|
return indexA - indexB;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const pokemonRows = computed<PokemonRow[]>(() => {
|
||||||
|
if (!habitat.value) return [];
|
||||||
|
|
||||||
|
const rows = new Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
timeOfDays: Set<string>;
|
||||||
|
weathers: Set<string>;
|
||||||
|
rarity: number;
|
||||||
|
maps: Set<string>;
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
|
||||||
|
habitat.value.pokemon.forEach((pokemon) => {
|
||||||
|
const key = `${pokemon.id}:${pokemon.rarity}`;
|
||||||
|
const row = rows.get(key) ?? {
|
||||||
|
id: pokemon.id,
|
||||||
|
name: pokemon.name,
|
||||||
|
timeOfDays: new Set<string>(),
|
||||||
|
weathers: new Set<string>(),
|
||||||
|
rarity: pokemon.rarity,
|
||||||
|
maps: new Set<string>()
|
||||||
|
};
|
||||||
|
|
||||||
|
row.timeOfDays.add(pokemon.time_of_day);
|
||||||
|
row.weathers.add(pokemon.weather);
|
||||||
|
row.maps.add(pokemon.map.name);
|
||||||
|
rows.set(key, row);
|
||||||
|
});
|
||||||
|
|
||||||
|
return [...rows.values()].map((row) => ({
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
timeOfDays: sortByOrder(row.timeOfDays, timeOfDays),
|
||||||
|
weathers: sortByOrder(row.weathers, weathers),
|
||||||
|
rarity: row.rarity,
|
||||||
|
maps: [...row.maps].sort((a, b) => a.localeCompare(b))
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
habitat.value = await api.habitatDetail(String(route.params.id));
|
habitat.value = await api.habitatDetail(String(route.params.id));
|
||||||
@@ -31,10 +95,27 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<section class="detail-section">
|
<section class="detail-section">
|
||||||
<h2>可能出现的宝可梦</h2>
|
<h2>可能出现的宝可梦</h2>
|
||||||
<ul class="row-list">
|
<ul class="row-list appearance-list">
|
||||||
<li v-for="item in habitat.pokemon" :key="`${item.id}-${item.map.id}-${item.time_of_day}-${item.weather}`">
|
<li v-for="item in pokemonRows" :key="`${item.id}-${item.rarity}`">
|
||||||
<RouterLink :to="`/pokemon/${item.id}`">{{ item.name }}</RouterLink>
|
<RouterLink class="appearance-name" :to="`/pokemon/${item.id}`">{{ item.name }}</RouterLink>
|
||||||
<span>{{ item.time_of_day }} · {{ item.weather }} · {{ item.rarity }} 星 · {{ item.map.name }}</span>
|
<dl class="appearance-summary">
|
||||||
|
<div>
|
||||||
|
<dt>时段</dt>
|
||||||
|
<dd>{{ item.timeOfDays.join(' / ') }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>天气</dt>
|
||||||
|
<dd>{{ item.weathers.join(' / ') }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>稀有度</dt>
|
||||||
|
<dd>{{ item.rarity }} 星</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>出现地图</dt>
|
||||||
|
<dd>{{ item.maps.join(' / ') }}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,11 +1,75 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import EntityChips from '../components/EntityChips.vue';
|
import EntityChips from '../components/EntityChips.vue';
|
||||||
import { api, type PokemonDetail } from '../services/api';
|
import { api, type PokemonDetail } from '../services/api';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const pokemon = ref<PokemonDetail | null>(null);
|
const pokemon = ref<PokemonDetail | null>(null);
|
||||||
|
const timeOfDays = ['早晨', '中午', '傍晚', '晚上'];
|
||||||
|
const weathers = ['晴天', '阴天', '雨天'];
|
||||||
|
|
||||||
|
type HabitatRow = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
timeOfDays: string[];
|
||||||
|
weathers: string[];
|
||||||
|
rarity: number;
|
||||||
|
maps: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function sortByOrder(values: Set<string>, order: string[]) {
|
||||||
|
return [...values].sort((a, b) => {
|
||||||
|
const indexA = order.indexOf(a);
|
||||||
|
const indexB = order.indexOf(b);
|
||||||
|
if (indexA === -1 && indexB === -1) return a.localeCompare(b);
|
||||||
|
if (indexA === -1) return 1;
|
||||||
|
if (indexB === -1) return -1;
|
||||||
|
return indexA - indexB;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const habitatRows = computed<HabitatRow[]>(() => {
|
||||||
|
if (!pokemon.value) return [];
|
||||||
|
|
||||||
|
const rows = new Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
timeOfDays: Set<string>;
|
||||||
|
weathers: Set<string>;
|
||||||
|
rarity: number;
|
||||||
|
maps: Set<string>;
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
|
||||||
|
pokemon.value.habitats.forEach((habitat) => {
|
||||||
|
const key = `${habitat.id}:${habitat.rarity}`;
|
||||||
|
const row = rows.get(key) ?? {
|
||||||
|
id: habitat.id,
|
||||||
|
name: habitat.name,
|
||||||
|
timeOfDays: new Set<string>(),
|
||||||
|
weathers: new Set<string>(),
|
||||||
|
rarity: habitat.rarity,
|
||||||
|
maps: new Set<string>()
|
||||||
|
};
|
||||||
|
|
||||||
|
row.timeOfDays.add(habitat.time_of_day);
|
||||||
|
row.weathers.add(habitat.weather);
|
||||||
|
row.maps.add(habitat.map.name);
|
||||||
|
rows.set(key, row);
|
||||||
|
});
|
||||||
|
|
||||||
|
return [...rows.values()].map((row) => ({
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
timeOfDays: sortByOrder(row.timeOfDays, timeOfDays),
|
||||||
|
weathers: sortByOrder(row.weathers, weathers),
|
||||||
|
rarity: row.rarity,
|
||||||
|
maps: [...row.maps].sort((a, b) => a.localeCompare(b))
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
pokemon.value = await api.pokemonDetail(String(route.params.id));
|
pokemon.value = await api.pokemonDetail(String(route.params.id));
|
||||||
@@ -36,10 +100,27 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<section class="detail-section">
|
<section class="detail-section">
|
||||||
<h2>栖息地</h2>
|
<h2>栖息地</h2>
|
||||||
<ul class="row-list">
|
<ul class="row-list appearance-list">
|
||||||
<li v-for="habitat in pokemon.habitats" :key="`${habitat.id}-${habitat.map.id}-${habitat.time_of_day}-${habitat.weather}`">
|
<li v-for="habitat in habitatRows" :key="`${habitat.id}-${habitat.rarity}`">
|
||||||
<RouterLink :to="`/habitats/${habitat.id}`">{{ habitat.name }}</RouterLink>
|
<RouterLink class="appearance-name" :to="`/habitats/${habitat.id}`">{{ habitat.name }}</RouterLink>
|
||||||
<span>{{ habitat.time_of_day }} · {{ habitat.weather }} · {{ habitat.rarity }} 星 · {{ habitat.map.name }}</span>
|
<dl class="appearance-summary">
|
||||||
|
<div>
|
||||||
|
<dt>时段</dt>
|
||||||
|
<dd>{{ habitat.timeOfDays.join(' / ') }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>天气</dt>
|
||||||
|
<dd>{{ habitat.weathers.join(' / ') }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>稀有度</dt>
|
||||||
|
<dd>{{ habitat.rarity }} 星</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>出现地图</dt>
|
||||||
|
<dd>{{ habitat.maps.join(' / ') }}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user