今日目標:完成一個星際地圖
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)
}
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 } // 切頁回頂
}
})
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>
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>
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>
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')
)。大家在測驗完這個小專案後,可以自己試試下面的練習,增強寫vue.js的能力喔~!
Home.vue
的卡片上加入 :class="{ active: $route.params.slug === p.slug }"
做「目前所在星球」高亮。/planet/jupiter/europa
子路由)。參考資源
https://vuejs.org/guide
https://www.runoob.com/vue3