我們今天要延續昨天的Final Mission — 5 日實作計畫,設計一個Galactic Explorer!
今日目標:展示宇宙星球資訊,能點選切換細節頁面。
今天會做出:
/planets:星球清單(v-for 渲染)/planets/:id:星球詳細(動態路由 + <transition>淡入)planets.json(含 8 大行星的基本常識)public/img/(Vite 會原樣輸出),JSON 放在 src/data/。
a) 建立資料夾
public/img/ # 圖片放這
src/data/ # JSON 放這
b) src/data/planets.json
(可先用這份,之後想再補充也行)
[
{
"id": 1,
"slug": "mercury",
"name": "水星",
"summary": "最接近太陽,溫差極大、沒有大氣保護。",
"image": "/img/mercury.jpg",
"facts": {
"英文名": "Mercury",
"類型": "類地行星",
"半徑": "2,440 km",
"公轉週期": "88 地球日",
"自轉週期": "~58.6 地球日",
"平均溫度": "-173 ~ 427°C",
"衛星數": "0",
"大氣": "極稀薄(幾乎沒有)",
"小知識": "遍布隕石坑,白天酷熱、夜晚極冷。"
}
},
{
"id": 2,
"slug": "venus",
"name": "金星",
"summary": "濃厚二氧化碳大氣與硫酸雲,溫室效應最強。",
"image": "/img/venus.jpg",
"facts": {
"英文名": "Venus",
"類型": "類地行星",
"半徑": "6,052 km",
"公轉週期": "225 地球日",
"自轉週期": "~243 地球日(逆行)",
"平均溫度": "~465°C",
"衛星數": "0",
"大氣": "以 CO₂ 為主,濃厚雲層",
"小知識": "自轉極慢且方向與多數行星相反。"
}
},
{
"id": 3,
"slug": "earth",
"name": "地球",
"summary": "液態海洋與可呼吸大氣,唯一已知有生命的行星。",
"image": "/img/earth.jpg",
"facts": {
"英文名": "Earth",
"類型": "類地行星",
"半徑": "6,371 km",
"公轉週期": "365.25 天",
"自轉週期": "24 小時",
"平均溫度": "~15°C",
"衛星數": "1(月球)",
"大氣": "氮氣、氧氣為主",
"小知識": "表面 70% 是海洋。"
}
},
{
"id": 4,
"slug": "mars",
"name": "火星",
"summary": "紅色沙塵世界,曾有液態水的跡象,殖民想像熱門。",
"image": "/img/mars.jpg",
"facts": {
"英文名": "Mars",
"類型": "類地行星",
"半徑": "3,390 km",
"公轉週期": "687 地球日",
"自轉週期": "24.6 小時",
"平均溫度": "-60°C",
"衛星數": "2(火衛一、火衛二)",
"大氣": "稀薄 CO₂",
"小知識": "擁有太陽系最高的火山奧林帕斯山。"
}
},
{
"id": 5,
"slug": "jupiter",
"name": "木星",
"summary": "太陽系最大行星,巨大的大紅斑風暴。",
"image": "/img/jupiter.jpg",
"facts": {
"英文名": "Jupiter",
"類型": "氣體巨行星",
"半徑": "69,911 km",
"公轉週期": "11.86 年",
"自轉週期": "~10 小時",
"平均溫度": "-110°C(雲頂)",
"衛星數": "95+",
"大氣": "氫、氦為主",
"小知識": "伽利略衛星之一的歐羅巴可能有地下海洋。"
}
},
{
"id": 6,
"slug": "saturn",
"name": "土星",
"summary": "最壯麗的行星環,冰與岩塊構成。",
"image": "/img/saturn.jpg",
"facts": {
"英文名": "Saturn",
"類型": "氣體巨行星",
"半徑": "58,232 km",
"公轉週期": "29.5 年",
"自轉週期": "~10.7 小時",
"平均溫度": "-140°C(雲頂)",
"衛星數": "145+",
"大氣": "氫、氦為主",
"小知識": "土衛六泰坦擁有濃厚大氣與甲烷湖。"
}
},
{
"id": 7,
"slug": "uranus",
"name": "天王星",
"summary": "自轉軸幾乎躺平,呈現側滾般的自轉。",
"image": "/img/uranus.jpg",
"facts": {
"英文名": "Uranus",
"類型": "冰巨行星",
"半徑": "25,362 km",
"公轉週期": "84 年",
"自轉週期": "~17.2 小時(逆行)",
"平均溫度": "-195°C(雲頂)",
"衛星數": "27+",
"大氣": "氫、氦、甲烷",
"小知識": "藍綠色來自甲烷吸收紅光。"
}
},
{
"id": 8,
"slug": "neptune",
"name": "海王星",
"summary": "最遠行星,強風與深藍色雲帶著名。",
"image": "/img/neptune.jpg",
"facts": {
"英文名": "Neptune",
"類型": "冰巨行星",
"半徑": "24,622 km",
"公轉週期": "164.8 年",
"自轉週期": "~16 小時",
"平均溫度": "-200°C(雲頂)",
"衛星數": "14+",
"大氣": "氫、氦、甲烷",
"小知識": "擁有太陽系最快的急流風(> 2,000 km/h)。"
}
}
]
圖片檔名可先放占位圖(ex: 從網路抓、或自己放空白圖),只要檔名對上即可。
在 src/router/index.js 增加一條動態路由(並建立對應檔案 PlanetDetail.vue):
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import Planets from '../views/Planets.vue'
import Diary from '../views/Diary.vue'
import Scanner from '../views/Scanner.vue'
import NotFound from '../views/NotFound.vue'
import PlanetDetail from '../views/PlanetDetail.vue'
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', name: 'home', component: Home },
{ path: '/planets', name: 'planets', component: Planets },
{ path: '/planets/:id', name: 'planet-detail', component: PlanetDetail, props: true },
{ path: '/diary', name: 'diary', component: Diary },
{ path: '/scanner', name: 'scanner', component: Scanner },
{ path: '/:pathMatch(.*)*', name: '404', component: NotFound }
],
scrollBehavior: () => ({ top: 0 })
})
export default router
/planets(src/views/Planets.vue)<template>
<section>
<h2>🗺️ 星球知識庫</h2>
<p class="dim">點擊卡片前往詳情頁。</p>
<div class="grid">
<article v-for="p in planets" :key="p.id" class="card planet">
<img :src="p.image" :alt="p.name" />
<h3>{{ p.name }}</h3>
<p class="sum">{{ p.summary }}</p>
<el-button type="primary" @click="$router.push(`/planets/${p.id}`)">
查看詳情
</el-button>
</article>
</div>
</section>
</template>
<script setup>
import planets from '../data/planets.json'
</script>
<style scoped>
.dim{ color: var(--text-dim); margin-bottom: 12px; }
.grid{
display: grid; gap: 16px;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}
.planet img{
width: 100%; height: 140px; object-fit: cover; border-radius: 12px;
margin-bottom: 8px;
}
.sum{ color: var(--text-dim); min-height: 44px; }
</style>
/planets/:id(src/views/PlanetDetail.vue)<template>
<transition name="fade">
<section v-if="planet" class="card detail">
<header class="head">
<img :src="planet.image" :alt="planet.name" />
<div>
<h2>{{ planet.name }}</h2>
<p class="dim">{{ planet.summary }}</p>
<el-button @click="$router.back()">← 返回列表</el-button>
</div>
</header>
<h3>📘 基本資料</h3>
<ul class="facts">
<li v-for="(val, key) in planet.facts" :key="key">
<span class="k">{{ key }}</span>
<span class="v">{{ val }}</span>
</li>
</ul>
</section>
</transition>
<section v-else class="card">
<h2>找不到這顆星球</h2>
<el-button type="primary" @click="$router.push('/planets')">回到列表</el-button>
</section>
</template>
<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import data from '../data/planets.json'
const route = useRoute()
const currentId = computed(() => Number(route.params.id))
const planet = computed(() => data.find(p => p.id === currentId.value))
</script>
<style scoped>
.detail { padding: 20px; }
.head { display: grid; grid-template-columns: 220px 1fr; gap: 16px; align-items: center; }
.head img { width: 100%; height: 180px; object-fit: cover; border-radius: 12px; }
.dim{ color: var(--text-dim); margin: 6px 0 12px; }
.facts{
list-style: none; margin: 0; padding: 0;
display: grid; gap: 8px;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}
.facts li{
display: grid; grid-template-columns: 120px 1fr; gap: 8px;
padding: 10px 12px; border:1px solid rgba(167,139,250,.2); border-radius: 12px;
background: #0f172a;
}
.k{ color:#a78bfa; }
.v{ color:#e0e7ff; }
/* Transition 淡入 */
.fade-enter-active, .fade-leave-active { transition: opacity .35s ease; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
</style>
說明:
<transition name="fade"> 控制淡入。planet.facts 採「key-value」列表顯示,增加可讀性。把行星圖片放到 public/img/,檔名對應 JSON,我有上傳一些星球圖片,大家可以參考下載:https://github.com/jaina0831/planets.git

最後完成今天的目標會長這樣,大家一樣可以自己美編,完成獨一無二的Galactic Explorer,明天我們將會把 Day 23 的 localStorage 技巧搬進 /diary,做出可新增、勾選、刪除、永久保存的日誌(搭配 Element Plus 的表單/卡片美化)
參考資源
https://vuejs.org/guide
https://www.runoob.com/vue3