路徑帶變數的路由,例如:/planet/:slug
:slug
會被解析成參數(e.g. mars
)。props: true
或 props: route => ({ slug: route.params.slug })
把參數當 props 傳給元件(較好測試、較好型別化)。/planet/mars/overview
、/planet/mars/moons
、/planet/mars/research
<RouterView />
當「內層跳躍門」。動態路由:資料集很多且同樣模板(每顆星球一頁)。
巢狀路由:同一顆星球裡有多個分頁(總覽/衛星/研究)。
A. 擴充路由:src/router/index.js
在 Day 17 的基礎上,給 planet
路由加入 children:
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import Planet from '../views/Planet.vue'
// 子頁面
import PlanetOverview from '../views/planet/Overview.vue'
import PlanetMoons from '../views/planet/Moons.vue'
import PlanetResearch from '../views/planet/Research.vue'
import NotFound from '../views/NotFound.vue'
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', name: 'home', component: Home },
{
path: '/planet/:slug',
name: 'planet',
component: Planet,
props: route => ({ slug: route.params.slug }),
children: [
{ path: '', redirect: { name: 'planet-overview' } },
{ path: 'overview', name: 'planet-overview', component: PlanetOverview, props: true },
{ path: 'moons', name: 'planet-moons', component: PlanetMoons, props: true },
{ path: 'research', name: 'planet-research', component: PlanetResearch, props: true },
]
},
{ path: '/:pathMatch(.*)*', name: '404', component: NotFound }
],
scrollBehavior: () => ({ top: 0 })
})
export default router
觀念重點:/planet/:slug
是父路由,底下的 overview / moons / research
是 子路由。URL 會變成 /planet/mars/moons
這種「多層級」路徑。
B. 父頁面要放內層 <RouterView />
:src/views/Planet.vue
在 Day 17 的 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>
<!-- 分頁導航 -->
<nav class="tabs">
<RouterLink :to="{ name: 'planet-overview', params: { slug } }">總覽</RouterLink>
<RouterLink :to="{ name: 'planet-moons', params: { slug } }">衛星</RouterLink>
<RouterLink :to="{ name: 'planet-research', params: { slug } }">研究</RouterLink>
</nav>
<!-- 巢狀子頁:若沒匹配到任何子頁,出現友善訊息 -->
<RouterView v-slot="{ Component }">
<component :is="Component" v-if="Component" :slug="slug" />
<p v-else class="hint">請從上方分頁選擇一個子頁面開始探索。</p>
</RouterView>
<!--(可選)偵錯:看到目前 slug 與是否有子頁 -->
<!-- <pre style="opacity:.5">slug={{ slug }}</pre> -->
</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'
const props = defineProps({ slug: String })
const slug = toRef(props, 'slug')
// 若 slug 找不到,planet 會是 undefined,會走到 v-else 分支
const planet = computed(() => getPlanetBySlug(slug.value))
</script>
<style scoped>
.wrap { max-width: 860px; margin: 40px auto; padding: 0 16px; }
.hero { display:flex; gap:16px; align-items:center; margin:12px 0 16px; }
.hero .emoji { font-size:56px; }
.summary { color:#475569; }
.tabs { display:flex; gap:12px; margin: 16px 0 24px; }
.tabs a { text-decoration:none; padding:6px 10px; border-radius:8px; border:1px solid #e2e8f0; color:#0f172a; }
.tabs a.router-link-active { background:#0f172a; color:#fff; border-color:#0f172a; }
.hint { color:#64748b; }
</style>
C. 建立子頁面
src/views/planet/Overview.vue
<template>
<section>
<h2>🛰️ 星球總覽</h2>
<ul>
<li>距離太陽:{{ planet.distance }}</li>
<li>表面重力:{{ planet.gravity }}</li>
<li>可居住性:{{ planet.habitable ? '🟢 候選' : '⚪ 尚不適宜' }}</li>
</ul>
<p style="color:#475569; margin-top:8px;">探索建議:{{ planet.tips }}</p>
</section>
</template>
<script setup>
import { computed } from 'vue'
import { getPlanetBySlug } from '../../data/planets'
const props = defineProps({ slug: String })
const planet = computed(() => getPlanetBySlug(props.slug))
</script>
src/views/planet/Moons.vue
<template>
<section>
<h2>🌙 主要衛星</h2>
<ul v-if="moons.length">
<li v-for="m in moons" :key="m.name">
{{ m.name }} — {{ m.note }}
</li>
</ul>
<p v-else>此星球暫無主要衛星或未收錄。</p>
</section>
</template>
<script setup>
const props = defineProps({ slug: String })
// 簡單示意資料
const db = {
earth: [{ name: '月球', note: '潮汐與自轉穩定器' }],
mars: [
{ name: '火衛一 Phobos', note: '近軌小衛星' },
{ name: '火衛二 Deimos', note: '遠軌小衛星' }
],
jupiter: [
{ name: '木衛一 Io', note: '火山活動活躍' },
{ name: '木衛二 Europa', note: '冰殼下的海洋' },
{ name: '木衛三 Ganymede', note: '太陽系最大衛星' },
{ name: '木衛四 Callisto', note: '布滿隕石坑' }
]
}
const moons = db[props.slug] || []
</script>
src/views/planet/Research.vue
<template>
<section>
<h2>🔬 研究任務</h2>
<p>填寫要在 {{ slug }} 進行的研究重點:</p>
<form @submit.prevent="add">
<input v-model.trim="draft" placeholder="例如:取樣分析 / 冰層鑽探" />
<button>加入</button>
</form>
<ul>
<li v-for="(t,i) in tasks" :key="i">{{ t }}</li>
</ul>
</section>
</template>
<script setup>
import { ref } from 'vue'
const props = defineProps({ slug: String })
const slug = props.slug
const draft = ref('')
const tasks = ref([])
function add() {
if (!draft.value) return
tasks.value.push(draft.value)
draft.value = ''
}
</script>
<style scoped>
form { display:flex; gap:8px; margin:10px 0; }
input { flex:1; padding:8px 10px; border:1px solid #e2e8f0; border-radius:8px; }
button { padding:8px 12px; border-radius:8px; border:1px solid #0f172a; background:#0f172a; color:#fff; }
</style>
用 props
取參數
在父/子頁面都用 props
接 slug
,比直接 useRoute()
更好測試,也利於型別與解耦。
參數變更時的反應
如果同一個元件會在不同參數間切換(例如從 /planet/mars
直接導航到 /planet/jupiter
而不卸載元件),你可以
import { watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
watch(() => route.params.slug, (newSlug) => { /* 重新抓資料 */ })
但我們這裡因為用 props
+ computed
,基本需求已足夠。
name
)<RouterLink :to="{ name:'planet-moons', params:{ slug } }">
<RouterView />
來顯示子頁。/planet/:slug/(overview|moons|research)
自由切換啦!參考資源
https://vuejs.org/guide
https://www.runoob.com/vue3