iT邦幫忙

2025 iThome 鐵人賽

DAY 17
0
Vue.js

邊學邊做:Vue.js 實戰養成計畫系列 第 17

Day 17:星際航路圖 — Vue Router 實作

  • 分享至 

  • xImage
  •  

今日目標:完成一個星際地圖

  • /:星際地圖(星球清單)
  • /planet/:slug:星球介紹頁(動態路由)
  • 找不到路由 → 404 頁

1) 建立資料:src/data/planets.js

export const planets = [
  {
    name: '水星',
    slug: 'mercury',
    emoji: '☿️',
    summary: '距離太陽最近的行星,晝夜溫差極大。',
    distance: '約 5800 萬公里',
    gravity: '約為地球的 0.38 倍',
    habitable: false,
    tips: '靠近太陽的輻射強,適合做耐熱材料與輻射屏蔽實驗。'
  },
  {
    name: '金星',
    slug: 'venus',
    emoji: '♀️',
    summary: '濃厚二氧化碳大氣與極端溫室效應,表面溫度可融鉛。',
    distance: '約 1.08 億公里',
    gravity: '約為地球的 0.9 倍',
    habitable: false,
    tips: '可研究極端大氣與雲頂飛行器技術。'
  },
  {
    name: '地球',
    slug: 'earth',
    emoji: '🌍',
    summary: '人類家園,藍色星球,液態水與生命豐富。',
    distance: '—',
    gravity: '1g',
    habitable: true,
    tips: '適合長期居住、補給與後勤中心。'
  },
  {
    name: '火星',
    slug: 'mars',
    emoji: '🔴',
    summary: '紅色沙土與極冠冰帽,地形多變,是移民熱門候選。',
    distance: '約 2.25 億公里(視會合更動)',
    gravity: '約為地球的 0.38 倍',
    habitable: true,
    tips: '需要氣壓/保溫/輻射遮蔽,適合溫室種植與地表漫遊。'
  },
  {
    name: '木星',
    slug: 'jupiter',
    emoji: '♃',
    summary: '太陽系最大行星,強大磁層與多衛星系統。',
    distance: '約 7.8 億公里',
    gravity: '約為地球的 2.5 倍(雲頂)',
    habitable: false,
    tips: '重點在衛星探索(如歐羅巴冰殼、伽利略衛星)。'
  }
]

// 小工具:由 slug 取星球
export function getPlanetBySlug(slug) {
  return planets.find(p => p.slug === slug)
}

2) 路由設定:src/router/index.js

import { createRouter, createWebHistory } from 'vue-router'

import Home from '../views/Home.vue'
import Planet from '../views/Planet.vue'
import NotFound from '../views/NotFound.vue'

const routes = [
  { path: '/', name: 'home', component: Home },
  // 將 :slug 轉成 props 傳進 Planet(等同 props: true,但我們保留擴充彈性)
  { path: '/planet/:slug', name: 'planet', component: Planet, props: route => ({ slug: route.params.slug }) },
  { path: '/:pathMatch(.*)*', name: '404', component: NotFound }
]

export default createRouter({
  history: createWebHistory(),
  routes,
  scrollBehavior() {
    return { top: 0 } // 切頁回頂
  }
})

3) 首頁(星際地圖):src/views/Home.vue

<template>
  <main class="wrap">
    <h1>🗺️ 星際地圖</h1>
    <p class="subtitle">點選星球,進入詳細介紹頁面。</p>

    <ul class="grid">
      <li v-for="p in planets" :key="p.slug" class="card">
        <RouterLink :to="{ name: 'planet', params: { slug: p.slug } }" class="link">
          <span class="emoji">{{ p.emoji }}</span>
          <h3>{{ p.name }}</h3>
          <p class="brief">{{ p.summary }}</p>
          <span class="badge" :class="{ good: p.habitable }">
            {{ p.habitable ? '🟢 可居住候選' : '⚪ 探索中' }}
          </span>
        </RouterLink>
      </li>
    </ul>
  </main>
</template>

<script setup>
import { planets } from '../data/planets'
</script>

<style scoped>
.wrap { max-width: 960px; margin: 40px auto; padding: 0 16px; font: 16px/1.7 ui-sans-serif, system-ui; }
.subtitle { color:#64748b; margin-top:-6px; }
.grid { display:grid; grid-template-columns: repeat(auto-fill, minmax(220px,1fr)); gap:16px; list-style:none; padding:0; }
.card { border:1px solid #e2e8f0; border-radius:14px; background:#fff; transition: transform .12s ease, box-shadow .12s ease; }
.card:hover { transform: translateY(-2px); box-shadow: 0 8px 24px #00000012; }
.link { display:block; padding:16px; text-decoration:none; color:inherit; }
.emoji { font-size:32px; display:block; }
.brief { color:#475569; min-height: 48px; }
.badge { display:inline-block; margin-top:8px; padding:4px 8px; border-radius:999px; font-size:12px; background:#e2e8f0; color:#0f172a; }
.badge.good { background:#dcfce7; color:#166534; }
</style>

4) 星球介紹頁(動態路由):src/views/Planet.vue

<template>
  <main class="wrap" v-if="planet">
    <RouterLink class="back" :to="{ name: 'home' }">← 返回星際地圖</RouterLink>

    <header class="hero">
      <div class="emoji">{{ planet.emoji }}</div>
      <div>
        <h1>{{ planet.name }}</h1>
        <p class="summary">{{ planet.summary }}</p>
      </div>
    </header>

    <section class="facts">
      <div class="fact"><span>距離太陽</span><strong>{{ planet.distance }}</strong></div>
      <div class="fact"><span>重力</span><strong>{{ planet.gravity }}</strong></div>
      <div class="fact">
        <span>可居住性</span>
        <strong :class="{ good: planet.habitable }">
          {{ planet.habitable ? '🟢 候選' : '⚪ 尚不適宜' }}
        </strong>
      </div>
    </section>

    <section class="tips">
      <h3>探索建議</h3>
      <p>{{ planet.tips }}</p>
    </section>
  </main>

  <main v-else class="wrap">
    <h1>找不到這顆星球</h1>
    <p>請返回 <RouterLink :to="{ name: 'home' }">星際地圖</RouterLink> 重新選擇。</p>
  </main>
</template>

<script setup>
import { toRef, computed } from 'vue'
import { getPlanetBySlug } from '../data/planets'

// 由路由 props 取得 slug(在 router/index.js 已 props 化)
const props = defineProps({ slug: String })
const slug = toRef(props, 'slug')

// 找到該星球
const planet = computed(() => getPlanetBySlug(slug.value))
</script>

<style scoped>
.wrap { max-width: 860px; margin: 40px auto; padding: 0 16px; font: 16px/1.7 ui-sans-serif, system-ui; }
.back { color:#2563eb; text-decoration:none; }
.hero { display:flex; gap:16px; align-items:center; margin:12px 0 16px; }
.hero .emoji { font-size:56px; }
.summary { color:#475569; }
.facts { display:grid; grid-template-columns: repeat(auto-fit,minmax(180px,1fr)); gap:12px; margin: 20px 0; }
.fact { border:1px solid #e2e8f0; border-radius:12px; padding:12px; background:#fff; }
.fact span { display:block; color:#64748b; font-size:12px; }
.fact strong { font-weight:700; }
.fact strong.good { color:#16a34a; }
.tips { border:1px solid #e2e8f0; border-radius:12px; padding:16px; background:#f8fafc; }
</style>

5) 404 頁:src/views/NotFound.vue

<template>
  <main class="wrap">
    <h1>🛰️ 迷失在宇宙航道</h1>
    <p>這條航線不存在,回到 <RouterLink :to="{ name: 'home' }">星際地圖</RouterLink> 吧!</p>
  </main>
</template>

<script setup></script>

<style scoped>
.wrap { max-width: 720px; margin: 40px auto; padding: 0 16px; font: 16px/1.7 ui-sans-serif, system-ui; }
</style>

6) App.vue(導覽列,保持簡潔)

<template>
  <header class="nav">
    <RouterLink to="/" class="brand">🚀 Orbit Coders</RouterLink>
  </header>
  <RouterView />
</template>

<style scoped>
.nav { display:flex; gap:16px; align-items:center; padding:12px 16px; border-bottom:1px solid #e2e8f0; background:#ffffff; position:sticky; top:0; z-index:10; }
.brand { text-decoration:none; font-weight:700; color:#0f172a; }
</style>

main.js 與 Day 16 相同(createApp(App).use(router).mount('#app'))。

7) 小延伸

大家在測驗完這個小專案後,可以自己試試下面的練習,增強寫vue.js的能力喔~!

  • Home.vue 的卡片上加入 :class="{ active: $route.params.slug === p.slug }" 做「目前所在星球」高亮。
  • 新增「土星」「天王星」;或把木星資訊延伸到其衛星(/planet/jupiter/europa 子路由)。

參考資源
https://vuejs.org/guide
https://www.runoob.com/vue3


上一篇
Day 16:星際航路圖 — Vue Router 入門
下一篇
Day 18:多重軌道 — 動態路由與巢狀路由
系列文
邊學邊做:Vue.js 實戰養成計畫18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言