Vue Router是Vue.js官方的路由管理器,用于构建单页面应用(SPA)。本章将详细介绍路由的配置、导航、参数传递和高级特性。

5.1 路由基础

安装和基本配置

# 安装Vue Router
npm install vue-router@4

# 或使用yarn
yarn add vue-router@4

# 或使用pnpm
pnpm add vue-router@4
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'
import Contact from '../views/Contact.vue'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  },
  {
    path: '/contact',
    name: 'Contact',
    component: Contact
  },
  {
    // 懒加载路由
    path: '/products',
    name: 'Products',
    component: () => import('../views/Products.vue')
  },
  {
    // 重定向
    path: '/home',
    redirect: '/'
  },
  {
    // 别名
    path: '/about-us',
    alias: '/about'
  },
  {
    // 404页面
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: () => import('../views/NotFound.vue')
  }
]

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes,
  // 滚动行为
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition
    } else {
      return { top: 0 }
    }
  }
})

export default router
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

const app = createApp(App)

app.use(router)
app.mount('#app')
<!-- App.vue -->
<template>
  <div id="app">
    <!-- 导航栏 -->
    <nav class="navbar">
      <div class="nav-brand">
        <router-link to="/" class="brand-link">My App</router-link>
      </div>
      <ul class="nav-menu">
        <li class="nav-item">
          <router-link to="/" class="nav-link">首页</router-link>
        </li>
        <li class="nav-item">
          <router-link to="/about" class="nav-link">关于</router-link>
        </li>
        <li class="nav-item">
          <router-link to="/products" class="nav-link">产品</router-link>
        </li>
        <li class="nav-item">
          <router-link to="/contact" class="nav-link">联系</router-link>
        </li>
      </ul>
    </nav>
    
    <!-- 路由视图 -->
    <main class="main-content">
      <router-view v-slot="{ Component, route }">
        <transition name="fade" mode="out-in">
          <component :is="Component" :key="route.path" />
        </transition>
      </router-view>
    </main>
    
    <!-- 页脚 -->
    <footer class="footer">
      <p>&copy; 2024 My App. All rights reserved.</p>
    </footer>
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>

<style>
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  line-height: 1.6;
  color: #333;
}

#app {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.navbar {
  background: #2c3e50;
  color: white;
  padding: 1rem 2rem;
  display: flex;
  justify-content: space-between;
  align-items: center;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

.nav-brand .brand-link {
  color: white;
  text-decoration: none;
  font-size: 1.5rem;
  font-weight: bold;
}

.nav-menu {
  display: flex;
  list-style: none;
  gap: 2rem;
}

.nav-link {
  color: white;
  text-decoration: none;
  padding: 0.5rem 1rem;
  border-radius: 4px;
  transition: background-color 0.3s;
}

.nav-link:hover {
  background-color: rgba(255,255,255,0.1);
}

.nav-link.router-link-active {
  background-color: #3498db;
}

.main-content {
  flex: 1;
  padding: 2rem;
}

.footer {
  background: #34495e;
  color: white;
  text-align: center;
  padding: 1rem;
}

/* 路由过渡动画 */
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

基本视图组件

<!-- views/Home.vue -->
<template>
  <div class="home">
    <div class="hero">
      <h1>欢迎来到我们的网站</h1>
      <p>这是一个使用Vue.js和Vue Router构建的单页面应用示例</p>
      <div class="hero-actions">
        <router-link to="/about" class="btn btn-primary">了解更多</router-link>
        <router-link to="/products" class="btn btn-secondary">查看产品</router-link>
      </div>
    </div>
    
    <div class="features">
      <div class="feature-card">
        <h3>响应式设计</h3>
        <p>适配各种设备和屏幕尺寸</p>
      </div>
      <div class="feature-card">
        <h3>现代技术栈</h3>
        <p>使用Vue 3和最新的前端技术</p>
      </div>
      <div class="feature-card">
        <h3>优秀性能</h3>
        <p>快速加载和流畅的用户体验</p>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Home',
  mounted() {
    console.log('Home组件已挂载')
  }
}
</script>

<style scoped>
.home {
  max-width: 1200px;
  margin: 0 auto;
}

.hero {
  text-align: center;
  padding: 4rem 2rem;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  border-radius: 8px;
  margin-bottom: 3rem;
}

.hero h1 {
  font-size: 3rem;
  margin-bottom: 1rem;
}

.hero p {
  font-size: 1.2rem;
  margin-bottom: 2rem;
  opacity: 0.9;
}

.hero-actions {
  display: flex;
  gap: 1rem;
  justify-content: center;
  flex-wrap: wrap;
}

.btn {
  padding: 0.75rem 1.5rem;
  border-radius: 4px;
  text-decoration: none;
  font-weight: 500;
  transition: all 0.3s;
}

.btn-primary {
  background: #3498db;
  color: white;
}

.btn-primary:hover {
  background: #2980b9;
  transform: translateY(-2px);
}

.btn-secondary {
  background: transparent;
  color: white;
  border: 2px solid white;
}

.btn-secondary:hover {
  background: white;
  color: #667eea;
}

.features {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 2rem;
  padding: 0 1rem;
}

.feature-card {
  background: white;
  padding: 2rem;
  border-radius: 8px;
  box-shadow: 0 4px 6px rgba(0,0,0,0.1);
  text-align: center;
  transition: transform 0.3s;
}

.feature-card:hover {
  transform: translateY(-4px);
}

.feature-card h3 {
  color: #2c3e50;
  margin-bottom: 1rem;
}

.feature-card p {
  color: #7f8c8d;
  line-height: 1.6;
}
</style>
<!-- views/About.vue -->
<template>
  <div class="about">
    <div class="about-header">
      <h1>关于我们</h1>
      <p>了解我们的故事和使命</p>
    </div>
    
    <div class="about-content">
      <div class="about-section">
        <h2>我们的使命</h2>
        <p>
          我们致力于通过创新的技术解决方案,为用户提供卓越的数字体验。
          我们相信技术的力量可以改变世界,让生活变得更加美好。
        </p>
      </div>
      
      <div class="about-section">
        <h2>我们的团队</h2>
        <div class="team-grid">
          <div class="team-member">
            <img src="https://via.placeholder.com/150" alt="团队成员">
            <h3>张三</h3>
            <p>首席执行官</p>
          </div>
          <div class="team-member">
            <img src="https://via.placeholder.com/150" alt="团队成员">
            <h3>李四</h3>
            <p>技术总监</p>
          </div>
          <div class="team-member">
            <img src="https://via.placeholder.com/150" alt="团队成员">
            <h3>王五</h3>
            <p>设计总监</p>
          </div>
        </div>
      </div>
      
      <div class="about-section">
        <h2>我们的价值观</h2>
        <ul class="values-list">
          <li>创新:持续探索新技术和解决方案</li>
          <li>质量:追求卓越,注重细节</li>
          <li>合作:团队协作,共同成长</li>
          <li>诚信:诚实守信,负责任</li>
        </ul>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'About'
}
</script>

<style scoped>
.about {
  max-width: 800px;
  margin: 0 auto;
  padding: 0 1rem;
}

.about-header {
  text-align: center;
  margin-bottom: 3rem;
}

.about-header h1 {
  font-size: 2.5rem;
  color: #2c3e50;
  margin-bottom: 1rem;
}

.about-header p {
  font-size: 1.2rem;
  color: #7f8c8d;
}

.about-section {
  margin-bottom: 3rem;
}

.about-section h2 {
  color: #2c3e50;
  margin-bottom: 1rem;
  border-bottom: 2px solid #3498db;
  padding-bottom: 0.5rem;
}

.about-section p {
  line-height: 1.8;
  color: #555;
}

.team-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 2rem;
  margin-top: 2rem;
}

.team-member {
  text-align: center;
  background: white;
  padding: 1.5rem;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}

.team-member img {
  width: 100px;
  height: 100px;
  border-radius: 50%;
  margin-bottom: 1rem;
  object-fit: cover;
}

.team-member h3 {
  color: #2c3e50;
  margin-bottom: 0.5rem;
}

.team-member p {
  color: #7f8c8d;
  font-size: 0.9rem;
}

.values-list {
  list-style: none;
  padding: 0;
}

.values-list li {
  background: #f8f9fa;
  padding: 1rem;
  margin-bottom: 0.5rem;
  border-left: 4px solid #3498db;
  border-radius: 4px;
}
</style>

5.2 动态路由和参数

路由参数

// router/index.js - 添加动态路由
const routes = [
  // ... 其他路由
  {
    // 用户详情页
    path: '/user/:id',
    name: 'UserDetail',
    component: () => import('../views/UserDetail.vue'),
    props: true // 将路由参数作为props传递给组件
  },
  {
    // 用户编辑页
    path: '/user/:id/edit',
    name: 'UserEdit',
    component: () => import('../views/UserEdit.vue'),
    props: true
  },
  {
    // 产品分类页
    path: '/category/:categoryId/products',
    name: 'CategoryProducts',
    component: () => import('../views/CategoryProducts.vue'),
    props: true
  },
  {
    // 产品详情页(多个参数)
    path: '/category/:categoryId/product/:productId',
    name: 'ProductDetail',
    component: () => import('../views/ProductDetail.vue'),
    props: true
  },
  {
    // 可选参数
    path: '/search/:keyword?',
    name: 'Search',
    component: () => import('../views/Search.vue'),
    props: true
  },
  {
    // 通配符参数
    path: '/files/:pathMatch(.*)*',
    name: 'FileExplorer',
    component: () => import('../views/FileExplorer.vue'),
    props: true
  }
]
<!-- views/UserDetail.vue -->
<template>
  <div class="user-detail">
    <div class="user-header">
      <button @click="goBack" class="back-btn">
        ← 返回
      </button>
      <h1>用户详情</h1>
    </div>
    
    <div v-if="loading" class="loading">
      加载中...
    </div>
    
    <div v-else-if="user" class="user-info">
      <div class="user-avatar">
        <img :src="user.avatar" :alt="user.name">
      </div>
      <div class="user-details">
        <h2>{{ user.name }}</h2>
        <p class="user-email">{{ user.email }}</p>
        <p class="user-bio">{{ user.bio }}</p>
        
        <div class="user-stats">
          <div class="stat">
            <span class="stat-value">{{ user.posts }}</span>
            <span class="stat-label">文章</span>
          </div>
          <div class="stat">
            <span class="stat-value">{{ user.followers }}</span>
            <span class="stat-label">关注者</span>
          </div>
          <div class="stat">
            <span class="stat-value">{{ user.following }}</span>
            <span class="stat-label">关注中</span>
          </div>
        </div>
        
        <div class="user-actions">
          <router-link 
            :to="{ name: 'UserEdit', params: { id: user.id } }"
            class="btn btn-primary"
          >
            编辑资料
          </router-link>
          <button @click="followUser" class="btn btn-secondary">
            {{ user.isFollowing ? '取消关注' : '关注' }}
          </button>
        </div>
      </div>
    </div>
    
    <div v-else class="error">
      <h2>用户不存在</h2>
      <p>抱歉,找不到ID为 {{ id }} 的用户。</p>
      <router-link to="/" class="btn btn-primary">返回首页</router-link>
    </div>
    
    <!-- 用户文章列表 -->
    <div v-if="user" class="user-posts">
      <h3>最近文章</h3>
      <div class="posts-grid">
        <div v-for="post in user.recentPosts" :key="post.id" class="post-card">
          <h4>{{ post.title }}</h4>
          <p>{{ post.excerpt }}</p>
          <div class="post-meta">
            <span>{{ formatDate(post.createdAt) }}</span>
            <span>{{ post.views }} 次阅读</span>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'UserDetail',
  props: {
    id: {
      type: String,
      required: true
    }
  },
  
  data() {
    return {
      user: null,
      loading: true
    }
  },
  
  async created() {
    await this.fetchUser()
  },
  
  watch: {
    // 监听路由参数变化
    id: {
      handler: 'fetchUser',
      immediate: true
    }
  },
  
  methods: {
    async fetchUser() {
      this.loading = true
      try {
        // 模拟API调用
        await new Promise(resolve => setTimeout(resolve, 1000))
        
        // 模拟用户数据
        const users = {
          '1': {
            id: '1',
            name: '张三',
            email: 'zhangsan@example.com',
            bio: '前端开发工程师,热爱Vue.js和现代Web技术。',
            avatar: 'https://via.placeholder.com/150',
            posts: 42,
            followers: 1234,
            following: 567,
            isFollowing: false,
            recentPosts: [
              {
                id: 1,
                title: 'Vue 3 Composition API 深度解析',
                excerpt: '详细介绍Vue 3 Composition API的使用方法和最佳实践...',
                createdAt: '2024-01-15',
                views: 1520
              },
              {
                id: 2,
                title: 'TypeScript在Vue项目中的应用',
                excerpt: '如何在Vue项目中有效使用TypeScript提升开发体验...',
                createdAt: '2024-01-10',
                views: 980
              }
            ]
          },
          '2': {
            id: '2',
            name: '李四',
            email: 'lisi@example.com',
            bio: 'UI/UX设计师,专注于用户体验设计。',
            avatar: 'https://via.placeholder.com/150',
            posts: 28,
            followers: 890,
            following: 234,
            isFollowing: true,
            recentPosts: [
              {
                id: 3,
                title: '现代Web设计趋势',
                excerpt: '2024年最新的Web设计趋势和用户体验设计原则...',
                createdAt: '2024-01-12',
                views: 756
              }
            ]
          }
        }
        
        this.user = users[this.id] || null
      } catch (error) {
        console.error('获取用户信息失败:', error)
        this.user = null
      } finally {
        this.loading = false
      }
    },
    
    goBack() {
      this.$router.go(-1)
    },
    
    followUser() {
      this.user.isFollowing = !this.user.isFollowing
      if (this.user.isFollowing) {
        this.user.followers++
      } else {
        this.user.followers--
      }
    },
    
    formatDate(dateString) {
      return new Date(dateString).toLocaleDateString('zh-CN')
    }
  }
}
</script>

<style scoped>
.user-detail {
  max-width: 800px;
  margin: 0 auto;
  padding: 0 1rem;
}

.user-header {
  display: flex;
  align-items: center;
  margin-bottom: 2rem;
}

.back-btn {
  background: none;
  border: none;
  font-size: 1rem;
  color: #3498db;
  cursor: pointer;
  margin-right: 1rem;
  padding: 0.5rem;
}

.back-btn:hover {
  background: #f8f9fa;
  border-radius: 4px;
}

.loading {
  text-align: center;
  padding: 3rem;
  font-size: 1.2rem;
  color: #7f8c8d;
}

.user-info {
  background: white;
  border-radius: 8px;
  padding: 2rem;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  margin-bottom: 2rem;
  display: flex;
  gap: 2rem;
}

.user-avatar img {
  width: 150px;
  height: 150px;
  border-radius: 50%;
  object-fit: cover;
}

.user-details {
  flex: 1;
}

.user-details h2 {
  color: #2c3e50;
  margin-bottom: 0.5rem;
}

.user-email {
  color: #7f8c8d;
  margin-bottom: 1rem;
}

.user-bio {
  color: #555;
  line-height: 1.6;
  margin-bottom: 1.5rem;
}

.user-stats {
  display: flex;
  gap: 2rem;
  margin-bottom: 1.5rem;
}

.stat {
  text-align: center;
}

.stat-value {
  display: block;
  font-size: 1.5rem;
  font-weight: bold;
  color: #2c3e50;
}

.stat-label {
  font-size: 0.9rem;
  color: #7f8c8d;
}

.user-actions {
  display: flex;
  gap: 1rem;
}

.btn {
  padding: 0.75rem 1.5rem;
  border: none;
  border-radius: 4px;
  text-decoration: none;
  cursor: pointer;
  font-size: 0.9rem;
  transition: all 0.3s;
}

.btn-primary {
  background: #3498db;
  color: white;
}

.btn-primary:hover {
  background: #2980b9;
}

.btn-secondary {
  background: #95a5a6;
  color: white;
}

.btn-secondary:hover {
  background: #7f8c8d;
}

.error {
  text-align: center;
  padding: 3rem;
}

.error h2 {
  color: #e74c3c;
  margin-bottom: 1rem;
}

.user-posts {
  background: white;
  border-radius: 8px;
  padding: 2rem;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}

.user-posts h3 {
  color: #2c3e50;
  margin-bottom: 1.5rem;
  border-bottom: 2px solid #3498db;
  padding-bottom: 0.5rem;
}

.posts-grid {
  display: grid;
  gap: 1.5rem;
}

.post-card {
  border: 1px solid #dee2e6;
  border-radius: 4px;
  padding: 1.5rem;
  transition: box-shadow 0.3s;
}

.post-card:hover {
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}

.post-card h4 {
  color: #2c3e50;
  margin-bottom: 0.5rem;
}

.post-card p {
  color: #555;
  line-height: 1.6;
  margin-bottom: 1rem;
}

.post-meta {
  display: flex;
  justify-content: space-between;
  font-size: 0.8rem;
  color: #7f8c8d;
}

@media (max-width: 768px) {
  .user-info {
    flex-direction: column;
    text-align: center;
  }
  
  .user-stats {
    justify-content: center;
  }
  
  .user-actions {
    justify-content: center;
  }
}
</style>

查询参数和路由状态

<!-- views/Search.vue -->
<template>
  <div class="search">
    <div class="search-header">
      <h1>搜索结果</h1>
      <div class="search-form">
        <input 
          v-model="searchQuery" 
          @input="updateSearch"
          @keyup.enter="performSearch"
          placeholder="输入搜索关键词..."
          class="search-input"
        >
        <button @click="performSearch" class="search-btn">搜索</button>
      </div>
    </div>
    
    <!-- 搜索过滤器 -->
    <div class="search-filters">
      <div class="filter-group">
        <label>类型:</label>
        <select v-model="filters.type" @change="updateFilters">
          <option value="">全部</option>
          <option value="article">文章</option>
          <option value="product">产品</option>
          <option value="user">用户</option>
        </select>
      </div>
      
      <div class="filter-group">
        <label>排序:</label>
        <select v-model="filters.sort" @change="updateFilters">
          <option value="relevance">相关性</option>
          <option value="date">日期</option>
          <option value="popularity">热度</option>
        </select>
      </div>
      
      <div class="filter-group">
        <label>时间:</label>
        <select v-model="filters.time" @change="updateFilters">
          <option value="">全部时间</option>
          <option value="day">最近一天</option>
          <option value="week">最近一周</option>
          <option value="month">最近一月</option>
          <option value="year">最近一年</option>
        </select>
      </div>
    </div>
    
    <!-- 搜索结果 -->
    <div class="search-results">
      <div class="results-info">
        <p v-if="keyword">
          搜索 "<strong>{{ keyword }}</strong>" 找到 {{ totalResults }} 个结果
          (用时 {{ searchTime }}ms)
        </p>
        <p v-else>
          请输入搜索关键词
        </p>
      </div>
      
      <div v-if="loading" class="loading">
        搜索中...
      </div>
      
      <div v-else-if="results.length > 0" class="results-list">
        <div 
          v-for="result in results" 
          :key="result.id" 
          class="result-item"
          :class="`result-${result.type}`"
        >
          <div class="result-header">
            <h3>
              <router-link :to="getResultLink(result)">
                {{ highlightKeyword(result.title) }}
              </router-link>
            </h3>
            <span class="result-type">{{ getTypeLabel(result.type) }}</span>
          </div>
          <p class="result-description">
            {{ highlightKeyword(result.description) }}
          </p>
          <div class="result-meta">
            <span class="result-date">{{ formatDate(result.date) }}</span>
            <span class="result-author">作者: {{ result.author }}</span>
            <span class="result-views">{{ result.views }} 次查看</span>
          </div>
        </div>
      </div>
      
      <div v-else-if="keyword" class="no-results">
        <h3>没有找到相关结果</h3>
        <p>尝试使用不同的关键词或调整搜索条件</p>
        <div class="suggestions">
          <h4>搜索建议:</h4>
          <ul>
            <li>检查拼写是否正确</li>
            <li>尝试使用更通用的关键词</li>
            <li>减少搜索词的数量</li>
            <li>尝试使用同义词</li>
          </ul>
        </div>
      </div>
    </div>
    
    <!-- 分页 -->
    <div v-if="totalPages > 1" class="pagination">
      <button 
        @click="goToPage(currentPage - 1)"
        :disabled="currentPage === 1"
        class="page-btn"
      >
        上一页
      </button>
      
      <span class="page-info">
        第 {{ currentPage }} 页,共 {{ totalPages }} 页
      </span>
      
      <button 
        @click="goToPage(currentPage + 1)"
        :disabled="currentPage === totalPages"
        class="page-btn"
      >
        下一页
      </button>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Search',
  props: {
    keyword: {
      type: String,
      default: ''
    }
  },
  
  data() {
    return {
      searchQuery: '',
      results: [],
      loading: false,
      totalResults: 0,
      searchTime: 0,
      currentPage: 1,
      pageSize: 10,
      filters: {
        type: '',
        sort: 'relevance',
        time: ''
      }
    }
  },
  
  computed: {
    totalPages() {
      return Math.ceil(this.totalResults / this.pageSize)
    }
  },
  
  watch: {
    keyword: {
      handler: 'initializeSearch',
      immediate: true
    },
    
    '$route.query': {
      handler: 'handleQueryChange',
      immediate: true
    }
  },
  
  methods: {
    initializeSearch() {
      this.searchQuery = this.keyword || ''
      if (this.keyword) {
        this.performSearch()
      }
    },
    
    handleQueryChange() {
      const query = this.$route.query
      
      // 更新过滤器状态
      this.filters.type = query.type || ''
      this.filters.sort = query.sort || 'relevance'
      this.filters.time = query.time || ''
      this.currentPage = parseInt(query.page) || 1
      
      // 如果有查询参数变化,重新搜索
      if (this.keyword) {
        this.performSearch()
      }
    },
    
    updateSearch() {
      // 防抖处理
      clearTimeout(this.searchTimeout)
      this.searchTimeout = setTimeout(() => {
        if (this.searchQuery !== this.keyword) {
          this.navigateToSearch()
        }
      }, 300)
    },
    
    navigateToSearch() {
      if (this.searchQuery.trim()) {
        this.$router.push({
          name: 'Search',
          params: { keyword: this.searchQuery.trim() },
          query: this.buildQuery()
        })
      }
    },
    
    updateFilters() {
      this.currentPage = 1
      this.$router.push({
        name: 'Search',
        params: { keyword: this.keyword },
        query: this.buildQuery()
      })
    },
    
    buildQuery() {
      const query = {}
      
      if (this.filters.type) query.type = this.filters.type
      if (this.filters.sort !== 'relevance') query.sort = this.filters.sort
      if (this.filters.time) query.time = this.filters.time
      if (this.currentPage > 1) query.page = this.currentPage
      
      return query
    },
    
    async performSearch() {
      if (!this.keyword) return
      
      this.loading = true
      const startTime = Date.now()
      
      try {
        // 模拟API调用
        await new Promise(resolve => setTimeout(resolve, 500))
        
        // 模拟搜索结果
        const mockResults = this.generateMockResults()
        this.results = mockResults.slice(
          (this.currentPage - 1) * this.pageSize,
          this.currentPage * this.pageSize
        )
        this.totalResults = mockResults.length
        this.searchTime = Date.now() - startTime
        
      } catch (error) {
        console.error('搜索失败:', error)
        this.results = []
        this.totalResults = 0
      } finally {
        this.loading = false
      }
    },
    
    generateMockResults() {
      const types = ['article', 'product', 'user']
      const results = []
      
      for (let i = 1; i <= 50; i++) {
        const type = types[Math.floor(Math.random() * types.length)]
        results.push({
          id: i,
          type,
          title: `${this.keyword} 相关${this.getTypeLabel(type)} ${i}`,
          description: `这是关于 ${this.keyword} 的详细描述,包含了相关的信息和内容...`,
          author: `作者${i}`,
          date: new Date(Date.now() - Math.random() * 365 * 24 * 60 * 60 * 1000).toISOString(),
          views: Math.floor(Math.random() * 10000)
        })
      }
      
      // 应用过滤器
      return results.filter(result => {
        if (this.filters.type && result.type !== this.filters.type) {
          return false
        }
        
        if (this.filters.time) {
          const now = new Date()
          const resultDate = new Date(result.date)
          const diffDays = (now - resultDate) / (1000 * 60 * 60 * 24)
          
          switch (this.filters.time) {
            case 'day': return diffDays <= 1
            case 'week': return diffDays <= 7
            case 'month': return diffDays <= 30
            case 'year': return diffDays <= 365
          }
        }
        
        return true
      }).sort((a, b) => {
        switch (this.filters.sort) {
          case 'date':
            return new Date(b.date) - new Date(a.date)
          case 'popularity':
            return b.views - a.views
          default:
            return 0
        }
      })
    },
    
    getTypeLabel(type) {
      const labels = {
        article: '文章',
        product: '产品',
        user: '用户'
      }
      return labels[type] || type
    },
    
    getResultLink(result) {
      const links = {
        article: `/article/${result.id}`,
        product: `/product/${result.id}`,
        user: `/user/${result.id}`
      }
      return links[result.type] || '#'
    },
    
    highlightKeyword(text) {
      if (!this.keyword) return text
      
      const regex = new RegExp(`(${this.keyword})`, 'gi')
      return text.replace(regex, '<mark>$1</mark>')
    },
    
    formatDate(dateString) {
      return new Date(dateString).toLocaleDateString('zh-CN')
    },
    
    goToPage(page) {
      if (page >= 1 && page <= this.totalPages) {
        this.currentPage = page
        this.$router.push({
          name: 'Search',
          params: { keyword: this.keyword },
          query: { ...this.buildQuery(), page }
        })
      }
    }
  }
}
</script>

<style scoped>
.search {
  max-width: 1000px;
  margin: 0 auto;
  padding: 0 1rem;
}

.search-header {
  margin-bottom: 2rem;
}

.search-header h1 {
  color: #2c3e50;
  margin-bottom: 1rem;
}

.search-form {
  display: flex;
  gap: 1rem;
  max-width: 600px;
}

.search-input {
  flex: 1;
  padding: 0.75rem;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 1rem;
}

.search-btn {
  padding: 0.75rem 1.5rem;
  background: #3498db;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 1rem;
}

.search-btn:hover {
  background: #2980b9;
}

.search-filters {
  display: flex;
  gap: 2rem;
  margin-bottom: 2rem;
  padding: 1rem;
  background: #f8f9fa;
  border-radius: 4px;
  flex-wrap: wrap;
}

.filter-group {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.filter-group label {
  font-weight: 500;
  color: #555;
}

.filter-group select {
  padding: 0.5rem;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.results-info {
  margin-bottom: 1.5rem;
  color: #666;
}

.loading {
  text-align: center;
  padding: 3rem;
  color: #7f8c8d;
  font-size: 1.1rem;
}

.results-list {
  margin-bottom: 2rem;
}

.result-item {
  background: white;
  border: 1px solid #dee2e6;
  border-radius: 4px;
  padding: 1.5rem;
  margin-bottom: 1rem;
  transition: box-shadow 0.3s;
}

.result-item:hover {
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}

.result-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  margin-bottom: 0.5rem;
}

.result-header h3 {
  margin: 0;
  flex: 1;
}

.result-header h3 a {
  color: #3498db;
  text-decoration: none;
}

.result-header h3 a:hover {
  text-decoration: underline;
}

.result-type {
  background: #e9ecef;
  color: #495057;
  padding: 0.25rem 0.5rem;
  border-radius: 12px;
  font-size: 0.8rem;
  margin-left: 1rem;
}

.result-description {
  color: #555;
  line-height: 1.6;
  margin-bottom: 1rem;
}

.result-meta {
  display: flex;
  gap: 1rem;
  font-size: 0.9rem;
  color: #7f8c8d;
  flex-wrap: wrap;
}

.no-results {
  text-align: center;
  padding: 3rem;
}

.no-results h3 {
  color: #e74c3c;
  margin-bottom: 1rem;
}

.suggestions {
  text-align: left;
  max-width: 400px;
  margin: 2rem auto 0;
}

.suggestions h4 {
  color: #2c3e50;
  margin-bottom: 1rem;
}

.suggestions ul {
  color: #555;
  line-height: 1.6;
}

.pagination {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 1rem;
  padding: 2rem 0;
}

.page-btn {
  padding: 0.5rem 1rem;
  border: 1px solid #ddd;
  border-radius: 4px;
  background: white;
  cursor: pointer;
}

.page-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.page-btn:hover:not(:disabled) {
  background: #f8f9fa;
}

.page-info {
  color: #666;
}

/* 高亮样式 */
:deep(mark) {
  background: #fff3cd;
  padding: 0 2px;
  border-radius: 2px;
}

@media (max-width: 768px) {
  .search-form {
    flex-direction: column;
  }
  
  .search-filters {
    flex-direction: column;
    gap: 1rem;
  }
  
  .result-header {
    flex-direction: column;
    gap: 0.5rem;
  }
  
  .result-type {
    margin-left: 0;
    align-self: flex-start;
  }
  
  .result-meta {
    flex-direction: column;
    gap: 0.5rem;
  }
}
</style>

本章小结

本章我们学习了Vue Router的核心功能:

  1. 路由基础:安装配置、基本路由定义和导航
  2. 动态路由:路由参数、查询参数和通配符
  3. 路由导航:编程式导航和声明式导航
  4. 路由状态:参数传递和状态管理

下一章预告

下一章我们将学习Vue Router的高级特性,包括: - 嵌套路由和路由守卫 - 路由元信息和过渡动画 - 路由懒加载和代码分割 - 路由最佳实践

练习题

基础练习

  1. 基本路由练习

    • 创建一个多页面应用
    • 实现基本的路由导航
    • 添加404页面处理
  2. 动态路由练习

    • 创建用户详情页
    • 实现路由参数传递
    • 添加路由参数验证

进阶练习

  1. 搜索功能练习

    • 实现搜索页面
    • 支持查询参数和过滤
    • 添加搜索历史功能
  2. 路由状态练习

    • 实现面包屑导航
    • 添加页面标题管理
    • 实现路由缓存机制

提示:路由是SPA应用的核心,理解路由的工作原理对构建复杂应用非常重要。