<RouterLink> 導到 /projects/:slug
useRoute() 讀取 :slug,從本地資料查單筆內容/projects/:slug/info、/projects/:slug/gallery
npm i vue-router@4
建立 src/router/index.ts:
// src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import Home from '@/views/Home.vue'
import ProjectDetail from '@/views/ProjectDetail.vue'
import NotFound from '@/views/NotFound.vue'
const routes: RouteRecordRaw[] = [
  { path: '/', name: 'home', component: Home },
  { path: '/projects/:slug', name: 'project-detail', component: ProjectDetail, props: true },
  { path: '/:pathMatch(.*)*', name: 'not-found', component: NotFound }
]
export const router = createRouter({
  history: createWebHistory(), // 若部署在子目錄,改 createWebHistory('/子路徑/')
  routes,
  scrollBehavior() {
    return { top: 0 }
  }
})
把 router 掛到 app(修改 src/main.ts):
import { createApp } from 'vue'
import App from './App.vue'
import { router } from './router'
import './styles/base.css'
createApp(App).use(router).mount('#app')
建立 src/views/Home.vue,把原本 App.vue 內主要內容移到這裡(Header/Footer 繼續在 App.vue):
<!-- src/views/Home.vue -->
<template>
  <main id="home">
    <Hero />
    <About />
    <Skills />
    <Projects />
    <Contact />
  </main>
</template>
<script setup lang="ts">
import Hero from '@/components/Hero.vue'
import About from '@/components/About.vue'
import Skills from '@/components/Skills.vue'
import Projects from '@/components/Projects.vue'
import Contact from '@/components/Contact.vue'
</script>
把 App.vue 改為只負責框架與 router 入口:
<!-- src/App.vue -->
<template>
  <SiteHeader />
  <RouterView />
  <SiteFooter />
</template>
<script setup lang="ts">
import { RouterView } from 'vue-router'
import SiteHeader from '@/components/SiteHeader.vue'
import SiteFooter from '@/components/SiteFooter.vue'
</script>
<RouterLink> 導向詳情頁修改 src/components/Projects.vue:把 demo / repo 留著,同時加一個「查看詳情」連到 /projects/:slug。
<!-- 片段:src/components/Projects.vue -->
<article class="card" v-for="p in view" :key="p.id">
  <h3>{{ p.title }}</h3>
  <p class="muted">{{ p.tech }}</p>
  <p>{{ p.desc }}</p>
  <div class="actions" style="display:flex; gap:8px; margin-top:8px;">
    <RouterLink class="btn small" :to="{ name:'project-detail', params:{ slug: p.slug } }">
      查看詳情
    </RouterLink>
    <a class="btn small btn-outline" :href="p.repo" target="_blank" rel="noopener">GitHub</a>
  </div>
</article>
為了 SSR / SEO 的一致性,建議只把「站內導覽」用 ,外部連結維持 。
slug 並顯示單筆專案建立 src/views/ProjectDetail.vue:
<template>
  <section class="container section" v-if="project">
    <nav style="margin-bottom:12px;">
      <RouterLink to="/" class="btn btn-outline">← 返回列表</RouterLink>
    </nav>
    <h2>{{ project.title }}</h2>
    <p class="muted">{{ project.tech }}</p>
    <div class="gallery" v-if="project.images?.length" style="display:flex; gap:12px; flex-wrap:wrap; margin:12px 0;">
      <img v-for="src in project.images" :key="src" :src="src" alt="專案截圖" width="360" />
    </div>
    <p>{{ project.desc }}</p>
    <div class="actions" style="display:flex; gap:8px; margin-top:8px;">
      <a class="btn" :href="project.demo" target="_blank" rel="noopener">Live Demo</a>
      <a class="btn btn-outline" :href="project.repo" target="_blank" rel="noopener">GitHub</a>
    </div>
  </section>
  <section class="container section" v-else>
    <h2>找不到這個專案</h2>
    <p class="muted">請回到列表,或確認網址是否正確。</p>
    <RouterLink to="/" class="btn">返回列表</RouterLink>
  </section>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { projects } from '@/data/projects'
const route = useRoute()
const slug = computed(() => String(route.params.slug || ''))
const project = computed(() => projects.find(p => p.slug === slug.value))
</script>
新增 src/views/NotFound.vue:
<template>
  <section class="container section">
    <h2>404 找不到頁面</h2>
    <p class="muted">你要找的頁面不存在,或已被移動。</p>
    <RouterLink to="/" class="btn">回首頁</RouterLink>
  </section>
</template>
路由中已加入 { path: '/:pathMatch(.)', component: NotFound }。
/projects/:slug/info、/projects/:slug/gallery如果想把詳情切成分頁(資訊/圖片):
src/views/ProjectInfo.vue、src/views/ProjectGallery.vue(可重用上面 ProjectDetail 裡的片段)src/router/index.ts):import ProjectInfo from '@/views/ProjectInfo.vue'
import ProjectGallery from '@/views/ProjectGallery.vue'
{
  path: '/projects/:slug',
  component: ProjectDetail, // 當父頁,內含次級 <RouterView />
  props: true,
  children: [
    { path: '', redirect: { name: 'project-info' } },
    { path: 'info', name: 'project-info', component: ProjectInfo, props: true },
    { path: 'gallery', name: 'project-gallery', component: ProjectGallery, props: true }
  ]
}
在 ProjectDetail.vue 中加入子導航與 <RouterView />:
<nav class="sub-nav" style="display:flex; gap:12px; margin:12px 0;">
  <RouterLink :to="{ name:'project-info', params:{ slug } }" active-class="active">資訊</RouterLink>
  <RouterLink :to="{ name:'project-gallery', params:{ slug } }" active-class="active">圖片</RouterLink>
</nav>
<RouterView />
/ 顯示 Home(含 Projects 列表)/projects/:slug 看到該專案詳細資料info / gallery
index.html
index.html 放頁面views/,由 Router 管<a href="/xxx"> 做站內導覽
<RouterLink :to="...">(SPA 體驗、保留狀態)createApp(App).mount('#app')
createApp(App).use(router).mount('#app')
assets 路徑;Vite 會把 src/assets 內的資源處理成相對路徑。表單與驗證(Vue 版):
v-model + 自訂驗證 改良 Contact 表單(即時錯誤、送出前檢查)