feat(home): add home page as main entry point

Introduce HomeView with quick links to wiki sections and community features
Update navigation, routing, and logo links to point to the new home page
This commit is contained in:
2026-05-03 17:46:36 +08:00
parent 6782ddd101
commit 6758aaaa7e
9 changed files with 693 additions and 6 deletions

View File

@@ -13,6 +13,7 @@ import {
iconDreamIsland,
iconEvent,
iconHabitat,
iconHome,
iconItem,
iconLife,
iconPokemon,
@@ -42,6 +43,7 @@ function can(permissionKey: string) {
const navItems = computed(() => {
const items = [
{ label: t('nav.home'), to: '/', icon: iconHome },
{ label: t('nav.pokemon'), to: '/pokemon', icon: iconPokemon },
{ label: t('nav.habitats'), to: '/habitats', icon: iconHabitat },
{ label: t('nav.items'), to: '/items', icon: iconItem },
@@ -87,7 +89,7 @@ async function logout() {
currentUser.value = null;
setAuthToken(null);
await router.push('/pokemon');
await router.push('/');
}
async function loadLanguages() {

View File

@@ -110,7 +110,7 @@ onBeforeUnmount(() => {
<Icon :icon="sidebarOpen ? iconClose : iconMenu" class="ui-icon" aria-hidden="true" />
</button>
<RouterLink class="brand-lockup brand-lockup--mobile" to="/pokemon" aria-label="Pokopia Wiki" @click="closeSidebar">
<RouterLink class="brand-lockup brand-lockup--mobile" to="/" aria-label="Pokopia Wiki" @click="closeSidebar">
<PokeBallMark size="34px" />
<span>
<span class="pokemon-word">Pokopia</span>
@@ -123,7 +123,7 @@ onBeforeUnmount(() => {
<aside id="app-sidebar" class="site-sidebar" :aria-label="t('nav.main')">
<div class="site-sidebar__inner">
<RouterLink class="brand-lockup" to="/pokemon" aria-label="Pokopia Wiki" @click="closeSidebar">
<RouterLink class="brand-lockup" to="/" aria-label="Pokopia Wiki" @click="closeSidebar">
<PokeBallMark size="42px" />
<span>
<span class="pokemon-word">Pokopia</span>

View File

@@ -20,6 +20,7 @@ export const iconEdit: AppIcon = 'mdi:pencil-outline';
export const iconError: AppIcon = 'mdi:close-circle-outline';
export const iconEvent: AppIcon = 'mdi:calendar-star';
export const iconHabitat: AppIcon = 'mdi:pine-tree';
export const iconHome: AppIcon = 'mdi:home-variant-outline';
export const iconImage: AppIcon = 'mdi:image-outline';
export const iconInfo: AppIcon = 'mdi:information-outline';
export const iconItem: AppIcon = 'mdi:bag-personal-outline';

View File

@@ -1,4 +1,5 @@
import { createRouter, createWebHistory } from 'vue-router';
import HomeView from '../views/HomeView.vue';
import PokemonList from '../views/PokemonList.vue';
import PokemonDetail from '../views/PokemonDetail.vue';
import HabitatList from '../views/HabitatList.vue';
@@ -25,7 +26,7 @@ const seo = (config: RouteSeoConfig) => config;
export const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', redirect: '/pokemon' },
{ path: '/', name: 'home', component: HomeView, meta: { seo: seo({ titleKey: 'pages.home.title', descriptionKey: 'pages.home.subtitle', canonicalPath: '/' }) } },
{ path: '/pokemon', name: 'pokemon-list', component: PokemonList, meta: { seo: seo({ titleKey: 'pages.pokemon.title', descriptionKey: 'pages.pokemon.subtitle' }) } },
{
path: '/pokemon/new',

View File

@@ -2,7 +2,7 @@ import type { RouteLocationNormalizedLoaded, Router } from 'vue-router';
import { getCurrentLocale, i18n, onLocaleChange } from './i18n';
const siteName = 'Pokopia Wiki';
const defaultCanonicalPath = '/pokemon';
const defaultCanonicalPath = '/';
const defaultImagePath = '/seo/pokopia-hero.jpg';
const fallbackSiteUrl = 'https://pokopiawiki.tootaio.com';

View File

@@ -4272,6 +4272,329 @@ button:disabled,
color: var(--muted);
}
.home-page {
display: grid;
gap: 28px;
}
.home-hero {
min-height: min(720px, calc(100dvh - 88px));
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(320px, 430px);
gap: 28px;
align-items: center;
}
.home-hero__copy,
.home-section,
.home-section__header,
.home-dex__screen,
.home-dex__copy {
display: grid;
}
.home-hero__copy {
gap: 18px;
align-content: center;
}
.home-hero__title {
max-width: 820px;
margin: 0;
color: var(--ink);
font-family: var(--font-display);
font-size: clamp(44px, 7vw, 82px);
font-weight: 950;
line-height: 0.98;
}
.home-hero__subtitle {
max-width: 68ch;
margin: 0;
color: var(--ink-soft);
font-size: 18px;
line-height: 1.62;
}
.home-hero__actions,
.home-quick-index,
.home-card-grid {
display: grid;
}
.home-hero__actions {
grid-template-columns: repeat(3, max-content);
gap: 10px;
align-items: center;
}
.home-quick-index {
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
max-width: 760px;
}
.home-quick-index a {
min-height: 72px;
display: grid;
align-content: center;
justify-items: start;
gap: 8px;
padding: 12px;
border: 1px solid var(--line);
border-radius: var(--radius-card);
background: var(--surface);
box-shadow: var(--shadow-soft);
color: var(--ink-soft);
font-weight: 900;
transition:
transform 0.16s ease,
border-color 0.16s ease,
color 0.16s ease,
box-shadow 0.16s ease;
}
.home-quick-index a:hover,
.home-card:hover {
transform: translateY(-2px);
border-color: var(--pokemon-blue);
box-shadow: 0 5px 0 var(--line-strong);
}
.home-quick-index .ui-icon {
width: 23px;
height: 23px;
color: var(--pokemon-blue);
}
.home-dex {
border: 4px solid #7b0f16;
border-radius: var(--radius-card);
background:
linear-gradient(90deg, rgba(255, 255, 255, 0.16) 0 20%, transparent 20% 100%),
linear-gradient(180deg, var(--pokemon-red) 0%, var(--pokemon-red-deep) 100%);
box-shadow: 0 8px 0 #7b0f16, var(--shadow-raised);
overflow: hidden;
}
.home-dex__head {
min-height: 60px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
padding: 14px 16px;
border-bottom: 4px solid #7b0f16;
color: #ffffff;
font-size: 13px;
font-weight: 950;
}
.home-dex__lights {
display: flex;
align-items: center;
gap: 8px;
}
.home-dex__lights span {
width: 16px;
height: 16px;
border: 2px solid var(--line-strong);
border-radius: 50%;
background: var(--pokemon-yellow);
box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.38);
}
.home-dex__lights span:first-child {
width: 30px;
height: 30px;
background: var(--pokemon-blue);
}
.home-dex__lights span:last-child {
background: var(--success);
}
.home-dex__screen {
gap: 18px;
justify-items: center;
margin: 16px;
min-height: 460px;
padding: 22px;
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;
color: #172036;
text-align: center;
}
.home-dex__copy {
gap: 8px;
max-width: 32ch;
}
.home-dex__copy strong {
font-family: var(--font-display);
font-size: 24px;
font-weight: 950;
line-height: 1.08;
}
.home-dex__copy p {
margin: 0;
color: #354052;
line-height: 1.55;
}
.home-dex__tiles {
width: 100%;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.home-dex__tiles a {
min-height: 74px;
display: grid;
justify-items: center;
align-content: center;
gap: 6px;
padding: 10px;
border: 2px solid rgba(23, 32, 54, 0.34);
border-radius: var(--radius-card);
background: #ffffff;
color: #172036;
font-weight: 950;
transition:
transform 0.16s ease,
border-color 0.16s ease,
box-shadow 0.16s ease;
}
.home-dex__tiles a:hover {
transform: translateY(-2px);
border-color: var(--pokemon-blue);
box-shadow: 0 3px 0 #172036;
}
.home-dex__tiles .ui-icon {
width: 24px;
height: 24px;
color: var(--pokemon-blue);
}
.home-section {
gap: 16px;
}
.home-section__header {
gap: 8px;
}
.home-section__header h2 {
margin: 0;
color: var(--ink);
font-family: var(--font-display);
font-size: clamp(28px, 4vw, 42px);
font-weight: 950;
line-height: 1.08;
}
.home-card-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 14px;
}
.home-card-grid--community {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.home-card-grid--future {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.home-card {
min-height: 170px;
display: grid;
align-content: start;
gap: 14px;
padding: 16px;
border: 2px solid var(--line-strong);
border-radius: var(--radius-card);
background: var(--surface);
box-shadow: var(--shadow-control);
color: var(--ink);
transition:
transform 0.16s ease,
border-color 0.16s ease,
box-shadow 0.16s ease;
}
.home-card--wide {
min-height: 148px;
grid-template-columns: auto minmax(0, 1fr);
align-items: start;
}
.home-card--future {
min-height: 154px;
}
.home-card__icon {
width: 54px;
height: 54px;
display: inline-grid;
place-items: center;
border: 2px solid var(--line-strong);
border-radius: var(--radius-control);
background: var(--pokemon-yellow);
box-shadow: 0 3px 0 var(--line-strong);
color: #172036;
}
.home-card:nth-child(2n) .home-card__icon {
background: var(--pokemon-blue);
color: #ffffff;
}
.home-card:nth-child(3n) .home-card__icon {
background: var(--surface-soft);
color: var(--pokemon-blue-deep);
}
.home-card__icon .ui-icon {
width: 27px;
height: 27px;
}
.home-card__copy {
min-width: 0;
display: grid;
gap: 7px;
}
.home-card__copy strong {
color: var(--ink);
font-family: var(--font-display);
font-size: 22px;
font-weight: 950;
line-height: 1.12;
overflow-wrap: anywhere;
}
.home-card__copy span {
color: var(--ink-soft);
line-height: 1.52;
overflow-wrap: anywhere;
}
.home-card--future .status-badge {
align-self: end;
}
.auth-page {
display: grid;
justify-items: center;
@@ -5233,6 +5556,7 @@ button:disabled,
.detail-grid,
.entity-profile-grid,
.home-hero,
.pokemon-image-detail,
.pokemon-profile-grid,
.pokemon-profile-row,
@@ -5300,6 +5624,19 @@ button:disabled,
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.home-hero {
min-height: auto;
}
.home-dex {
max-width: 560px;
}
.home-card-grid,
.home-card-grid--future {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.appearance-row__main {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@@ -5334,12 +5671,47 @@ button:disabled,
.toolbar,
.entity-grid,
.grid,
.home-card-grid,
.home-card-grid--community,
.home-card-grid--future,
.home-hero__actions,
.home-quick-index,
.pokemon-fetch-panel,
.pokemon-edit-grid,
.coming-soon-preview {
grid-template-columns: 1fr;
}
.home-page {
gap: 24px;
}
.home-hero {
gap: 22px;
}
.home-hero__title {
font-size: 40px;
}
.home-hero__subtitle {
font-size: 16px;
}
.home-hero__actions .ui-button {
width: 100%;
}
.home-card--wide {
grid-template-columns: 1fr;
}
.home-dex__screen {
min-height: 420px;
margin: 12px;
padding: 16px;
}
.entity-card {
grid-template-columns: 1fr;
}

View File

@@ -0,0 +1,168 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import PokeBallMark from '../components/PokeBallMark.vue';
import StatusBadge from '../components/StatusBadge.vue';
import {
iconAction,
iconAutomation,
iconChecklist,
iconClothes,
iconDish,
iconDreamIsland,
iconEvent,
iconHabitat,
iconItem,
iconLife,
iconPokemon,
iconRecipe
} from '../icons';
const { t } = useI18n();
const primarySections = computed(() => [
{ key: 'pokemon', to: '/pokemon', icon: iconPokemon },
{ key: 'habitats', to: '/habitats', icon: iconHabitat },
{ key: 'items', to: '/items', icon: iconItem },
{ key: 'recipes', to: '/recipes', icon: iconRecipe }
]);
const communitySections = computed(() => [
{ key: 'checklist', to: '/checklist', icon: iconChecklist },
{ key: 'life', to: '/life', icon: iconLife }
]);
const futureSections = computed(() => [
{ key: 'automation', to: '/automation', icon: iconAutomation },
{ key: 'dish', to: '/dish', icon: iconDish },
{ key: 'events', to: '/events', icon: iconEvent },
{ key: 'actions', to: '/actions', icon: iconAction },
{ key: 'dreamIsland', to: '/dream-island', icon: iconDreamIsland },
{ key: 'clothes', to: '/clothes', icon: iconClothes }
]);
function sectionTitleKey(key: string) {
return `pages.home.sections.${key}.title`;
}
function sectionDescriptionKey(key: string) {
return `pages.home.sections.${key}.description`;
}
</script>
<template>
<section class="home-page">
<section class="home-hero" aria-labelledby="home-title">
<div class="home-hero__copy">
<span class="page-kicker">{{ t('pages.home.kicker') }}</span>
<h1 id="home-title" class="home-hero__title">{{ t('pages.home.title') }}</h1>
<p class="home-hero__subtitle">{{ t('pages.home.subtitle') }}</p>
<div class="home-hero__actions" :aria-label="t('pages.home.primaryActions')">
<RouterLink class="ui-button ui-button--primary" to="/pokemon">
<Icon :icon="iconPokemon" class="ui-icon" aria-hidden="true" />
{{ t('pages.home.browsePokemon') }}
</RouterLink>
<RouterLink class="ui-button ui-button--blue" to="/checklist">
<Icon :icon="iconChecklist" class="ui-icon" aria-hidden="true" />
{{ t('pages.home.openChecklist') }}
</RouterLink>
<RouterLink class="ui-button ui-button--ghost" to="/life">
<Icon :icon="iconLife" class="ui-icon" aria-hidden="true" />
{{ t('pages.home.openLife') }}
</RouterLink>
</div>
<div class="home-quick-index" :aria-label="t('pages.home.quickIndex')">
<RouterLink v-for="section in primarySections" :key="section.key" :to="section.to">
<Icon :icon="section.icon" class="ui-icon" aria-hidden="true" />
<span>{{ t(sectionTitleKey(section.key)) }}</span>
</RouterLink>
</div>
</div>
<aside class="home-dex" :aria-label="t('pages.home.featuredPanel')">
<div class="home-dex__head">
<div class="home-dex__lights" aria-hidden="true">
<span></span>
<span></span>
<span></span>
</div>
<span>{{ t('pages.home.dexCode') }}</span>
</div>
<div class="home-dex__screen">
<PokeBallMark size="84px" />
<div class="home-dex__copy">
<strong>{{ t('pages.home.dexTitle') }}</strong>
<p>{{ t('pages.home.dexBody') }}</p>
</div>
<div class="home-dex__tiles">
<RouterLink v-for="section in primarySections" :key="section.key" :to="section.to">
<Icon :icon="section.icon" class="ui-icon" aria-hidden="true" />
<span>{{ t(sectionTitleKey(section.key)) }}</span>
</RouterLink>
</div>
</div>
</aside>
</section>
<section class="home-section" aria-labelledby="home-wiki-title">
<div class="home-section__header">
<span class="page-kicker">{{ t('pages.home.wikiKicker') }}</span>
<h2 id="home-wiki-title">{{ t('pages.home.wikiTitle') }}</h2>
</div>
<div class="home-card-grid home-card-grid--primary">
<RouterLink v-for="section in primarySections" :key="section.key" class="home-card" :to="section.to">
<span class="home-card__icon">
<Icon :icon="section.icon" class="ui-icon" aria-hidden="true" />
</span>
<span class="home-card__copy">
<strong>{{ t(sectionTitleKey(section.key)) }}</strong>
<span>{{ t(sectionDescriptionKey(section.key)) }}</span>
</span>
</RouterLink>
</div>
</section>
<section class="home-section" aria-labelledby="home-community-title">
<div class="home-section__header">
<span class="page-kicker">{{ t('pages.home.communityKicker') }}</span>
<h2 id="home-community-title">{{ t('pages.home.communityTitle') }}</h2>
</div>
<div class="home-card-grid home-card-grid--community">
<RouterLink v-for="section in communitySections" :key="section.key" class="home-card home-card--wide" :to="section.to">
<span class="home-card__icon">
<Icon :icon="section.icon" class="ui-icon" aria-hidden="true" />
</span>
<span class="home-card__copy">
<strong>{{ t(sectionTitleKey(section.key)) }}</strong>
<span>{{ t(sectionDescriptionKey(section.key)) }}</span>
</span>
</RouterLink>
</div>
</section>
<section class="home-section" aria-labelledby="home-future-title">
<div class="home-section__header">
<span class="page-kicker">{{ t('pages.home.futureKicker') }}</span>
<h2 id="home-future-title">{{ t('pages.home.futureTitle') }}</h2>
</div>
<div class="home-card-grid home-card-grid--future">
<RouterLink v-for="section in futureSections" :key="section.key" class="home-card home-card--future" :to="section.to">
<span class="home-card__icon">
<Icon :icon="section.icon" class="ui-icon" aria-hidden="true" />
</span>
<span class="home-card__copy">
<strong>{{ t(sectionTitleKey(section.key)) }}</strong>
<span>{{ t(sectionDescriptionKey(section.key)) }}</span>
</span>
<StatusBadge :label="t('common.inDev')" tone="info" compact />
</RouterLink>
</div>
</section>
</section>
</template>