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>© 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的核心功能:
- 路由基础:安装配置、基本路由定义和导航
- 动态路由:路由参数、查询参数和通配符
- 路由导航:编程式导航和声明式导航
- 路由状态:参数传递和状态管理
下一章预告
下一章我们将学习Vue Router的高级特性,包括: - 嵌套路由和路由守卫 - 路由元信息和过渡动画 - 路由懒加载和代码分割 - 路由最佳实践
练习题
基础练习
基本路由练习:
- 创建一个多页面应用
- 实现基本的路由导航
- 添加404页面处理
动态路由练习:
- 创建用户详情页
- 实现路由参数传递
- 添加路由参数验证
进阶练习
搜索功能练习:
- 实现搜索页面
- 支持查询参数和过滤
- 添加搜索历史功能
路由状态练习:
- 实现面包屑导航
- 添加页面标题管理
- 实现路由缓存机制
提示:路由是SPA应用的核心,理解路由的工作原理对构建复杂应用非常重要。