路由管理
2025年5月28日大约 5 分鐘
學習內容
- 理解 Nuxt.js 檔案系統路由概念
- 掌握靜態與動態路由建立
- 學會使用 NuxtLink 進行頁面導航
- 理解路由參數處理
- 路由中介軟體與驗證
1. 檔案系統路由簡介
核心概念
Nuxt.js 使用檔案系統路由,pages/
目錄中的每個 Vue 檔案會自動建立對應的 URL 路由。這基於 vue-router 並支援程式碼分割,只載入當前路由所需的 JavaScript。
路由命名規則
pages/
├── index.vue → /
├── about.vue → /about
├── contact.vue → /contact
└── posts/
├── index.vue → /posts
├── [id].vue → /posts/:id (動態路由)
└── create.vue → /posts/create
自動生成的路由配置
{
"routes": [
{ "path": "/", "component": "pages/index.vue" },
{ "path": "/about", "component": "pages/about.vue" },
{ "path": "/posts", "component": "pages/posts/index.vue" },
{ "path": "/posts/:id", "component": "pages/posts/[id].vue" }
]
}
2. 建立基本頁面
首頁
<!-- pages/index.vue -->
<template>
<div>
<h1>歡迎來到電機系實驗室管理系統</h1>
<p>選擇下列功能:</p>
<nav>
<ul>
<li><NuxtLink to="/equipment">設備管理</NuxtLink></li>
<li><NuxtLink to="/students">學生管理</NuxtLink></li>
<li><NuxtLink to="/projects">專題管理</NuxtLink></li>
</ul>
</nav>
</div>
</template>
<style scoped>
nav ul {
list-style: none;
padding: 0;
}
nav li {
margin: 10px 0;
}
nav a {
display: inline-block;
padding: 10px 20px;
background-color: #3498db;
color: white;
text-decoration: none;
border-radius: 5px;
}
nav a:hover {
background-color: #2980b9;
}
</style>
設備管理頁面
<!-- pages/equipment/index.vue -->
<template>
<div>
<h1>設備管理</h1>
<div class="equipment-list">
<div class="equipment-card" v-for="item in equipment" :key="item.id">
<h3>{{ item.name }}</h3>
<p>編號:{{ item.id }}</p>
<p>狀態:{{ item.status }}</p>
<NuxtLink :to="`/equipment/${item.id}`">查看詳情</NuxtLink>
</div>
</div>
</div>
</template>
<script setup>
const equipment = [
{ id: 'OSC001', name: '數位示波器', status: '可用' },
{ id: 'GEN002', name: '函數產生器', status: '維修中' },
{ id: 'PSU003', name: '直流電源供應器', status: '可用' }
]
</script>
<style scoped>
.equipment-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
.equipment-card {
border: 1px solid #ddd;
padding: 20px;
border-radius: 8px;
background-color: #f9f9f9;
}
.equipment-card a {
display: inline-block;
margin-top: 10px;
padding: 8px 16px;
background-color: #27ae60;
color: white;
text-decoration: none;
border-radius: 4px;
}
</style>
3. 動態路由與參數處理
動態路由頁面
<!-- pages/equipment/[id].vue -->
<template>
<div>
<NuxtLink to="/equipment">← 返回設備列表</NuxtLink>
<div v-if="equipment" class="equipment-detail">
<h1>{{ equipment.name }}</h1>
<div class="detail-grid">
<div class="info-section">
<h3>基本資訊</h3>
<p><strong>設備編號:</strong>{{ equipment.id }}</p>
<p><strong>型號:</strong>{{ equipment.model }}</p>
<p><strong>製造商:</strong>{{ equipment.manufacturer }}</p>
<p><strong>狀態:</strong>
<span :class="statusClass">{{ equipment.status }}</span>
</p>
</div>
<div class="specs-section">
<h3>規格參數</h3>
<ul>
<li v-for="spec in equipment.specifications" :key="spec.name">
<strong>{{ spec.name }}:</strong>{{ spec.value }}
</li>
</ul>
</div>
</div>
<div class="actions">
<button @click="borrowEquipment" :disabled="equipment.status !== '可用'">
借用設備
</button>
<button @click="reportIssue">回報問題</button>
</div>
</div>
<div v-else class="not-found">
<h2>找不到設備</h2>
<p>設備編號 "{{ route.params.id }}" 不存在</p>
</div>
</div>
</template>
<script setup>
const route = useRoute()
// 模擬設備資料庫
const equipmentDatabase = {
'OSC001': {
id: 'OSC001',
name: '數位示波器',
model: 'DS1054Z',
manufacturer: 'Rigol',
status: '可用',
specifications: [
{ name: '頻寬', value: '50MHz' },
{ name: '取樣率', value: '1GSa/s' },
{ name: '通道數', value: '4' }
]
},
'GEN002': {
id: 'GEN002',
name: '函數產生器',
model: 'DG1032Z',
manufacturer: 'Rigol',
status: '維修中',
specifications: [
{ name: '頻率範圍', value: '1μHz - 30MHz' },
{ name: '波形', value: '正弦波、方波、三角波' },
{ name: '輸出電壓', value: '1mVpp - 10Vpp' }
]
}
}
// 根據路由參數獲取設備資料
const equipment = computed(() => {
return equipmentDatabase[route.params.id]
})
// 計算狀態樣式
const statusClass = computed(() => ({
'status-available': equipment.value?.status === '可用',
'status-maintenance': equipment.value?.status === '維修中',
'status-borrowed': equipment.value?.status === '已借出'
}))
// 借用設備功能
const borrowEquipment = () => {
alert(`已申請借用 ${equipment.value.name}`)
}
// 回報問題功能
const reportIssue = () => {
alert('問題回報功能開發中...')
}
console.log('當前設備ID:', route.params.id)
</script>
<style scoped>
.equipment-detail {
max-width: 800px;
margin: 20px 0;
}
.detail-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin: 20px 0;
}
.info-section, .specs-section {
background-color: #f8f9fa;
padding: 20px;
border-radius: 8px;
}
.specs-section ul {
list-style: none;
padding: 0;
}
.specs-section li {
margin: 8px 0;
}
.status-available {
color: #27ae60;
font-weight: bold;
}
.status-maintenance {
color: #e74c3c;
font-weight: bold;
}
.status-borrowed {
color: #f39c12;
font-weight: bold;
}
.actions {
margin-top: 30px;
}
.actions button {
margin-right: 10px;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
}
.actions button:first-child {
background-color: #3498db;
color: white;
}
.actions button:last-child {
background-color: #95a5a6;
color: white;
}
.actions button:disabled {
background-color: #bdc3c7;
cursor: not-allowed;
}
.not-found {
text-align: center;
padding: 40px;
background-color: #f8f9fa;
border-radius: 8px;
}
</style>
4. 進階導航功能
程式化導航
<!-- pages/students/index.vue -->
<template>
<div>
<h1>學生管理</h1>
<div class="search-section">
<input
v-model="searchQuery"
placeholder="搜尋學生姓名或學號"
@keyup.enter="searchStudents"
>
<button @click="searchStudents">搜尋</button>
</div>
<div class="students-grid">
<div
v-for="student in filteredStudents"
:key="student.id"
class="student-card"
@click="viewStudent(student.id)"
>
<h3>{{ student.name }}</h3>
<p>學號:{{ student.id }}</p>
<p>年級:{{ student.grade }}</p>
</div>
</div>
</div>
</template>
<script setup>
const searchQuery = ref('')
const students = [
{ id: 'E1001', name: '張小明', grade: '大四' },
{ id: 'E1002', name: '李小華', grade: '大三' },
{ id: 'E1003', name: '王小美', grade: '大二' }
]
const filteredStudents = computed(() => {
if (!searchQuery.value) return students
return students.filter(student =>
student.name.includes(searchQuery.value) ||
student.id.includes(searchQuery.value)
)
})
const searchStudents = () => {
// 搜尋邏輯已在 computed 中處理
console.log('搜尋:', searchQuery.value)
}
const viewStudent = (studentId) => {
// 程式化導航
navigateTo(`/students/${studentId}`)
}
</script>
<style scoped>
.search-section {
margin: 20px 0;
}
.search-section input {
padding: 10px;
width: 300px;
margin-right: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
.search-section button {
padding: 10px 20px;
background-color: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.students-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
margin-top: 20px;
}
.student-card {
border: 1px solid #ddd;
padding: 15px;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
}
.student-card:hover {
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
transform: translateY(-2px);
}
</style>
5. 路由中介軟體與驗證
認證中介軟體
// middleware/auth.js
export default defineNuxtRouteMiddleware((to, from) => {
// 模擬認證檢查
const isAuthenticated = () => {
// 在實際應用中,這裡會檢查 token 或 session
return localStorage.getItem('userToken') !== null
}
if (!isAuthenticated()) {
console.log('未登入,重導向至登入頁面')
return navigateTo('/login')
}
})
受保護的頁面
<!-- pages/admin/dashboard.vue -->
<template>
<div>
<h1>管理員儀表板</h1>
<div class="dashboard-stats">
<div class="stat-card">
<h3>設備總數</h3>
<p class="stat-number">24</p>
</div>
<div class="stat-card">
<h3>學生人數</h3>
<p class="stat-number">156</p>
</div>
<div class="stat-card">
<h3>進行中專題</h3>
<p class="stat-number">12</p>
</div>
</div>
</div>
</template>
<script setup>
// 使用認證中介軟體保護此頁面
definePageMeta({
middleware: 'auth'
})
</script>
<style scoped>
.dashboard-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-top: 30px;
}
.stat-card {
background-color: #3498db;
color: white;
padding: 30px;
border-radius: 8px;
text-align: center;
}
.stat-number {
font-size: 3rem;
font-weight: bold;
margin: 10px 0;
}
</style>
路由驗證
<!-- pages/projects/[id].vue -->
<template>
<div>
<h1>專題詳情</h1>
<div v-if="project">
<h2>{{ project.title }}</h2>
<p><strong>負責學生:</strong>{{ project.student }}</p>
<p><strong>指導教授:</strong>{{ project.advisor }}</p>
<p><strong>進度:</strong>{{ project.progress }}%</p>
<div class="progress-bar">
<div
class="progress-fill"
:style="{ width: project.progress + '%' }"
></div>
</div>
</div>
</div>
</template>
<script setup>
const route = useRoute()
// 路由驗證:檢查專題 ID 格式
definePageMeta({
validate: async (route) => {
// 檢查 ID 是否為 P 開頭加上數字
return typeof route.params.id === 'string' && /^P\d+$/.test(route.params.id)
}
})
// 模擬專題資料
const projects = {
'P001': {
title: '智慧電網監控系統',
student: '張小明',
advisor: '李教授',
progress: 75
},
'P002': {
title: '無線感測網路設計',
student: '王小華',
advisor: '陳教授',
progress: 60
}
}
const project = computed(() => {
return projects[route.params.id]
})
</script>
<style scoped>
.progress-bar {
width: 100%;
height: 20px;
background-color: #ecf0f1;
border-radius: 10px;
overflow: hidden;
margin-top: 15px;
}
.progress-fill {
height: 100%;
background-color: #27ae60;
transition: width 0.3s ease;
}
</style>