路由系统
上一章节我们的任务管理逻辑已经通过 Pinia 实现了持久化,现在我们需要为应用注入灵魂——路由系统。
在企业级开发中,路由不只是切换页面,更承担了安全性(权限控制)和用户体验(动态标题)的重任。
安装与基础配置
首先安装 Vue Router:
npm install vue-router
在 src 目录下创建 router 目录,并创建 router/index.js 文件。

我们将定义两个页面:工作台 (Dashboard) 和 登录页 (Login)。
实例
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: { title: '登录 - TaskHub', requiresAuth: false }
},
{
path: '/',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
// 路由元信息 meta:用来存放自定义数据
meta: {
title: '我的工作台',
requiresAuth: true,
breadcrumb: '首页'
}
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: { title: '登录 - TaskHub', requiresAuth: false }
},
{
path: '/',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
// 路由元信息 meta:用来存放自定义数据
meta: {
title: '我的工作台',
requiresAuth: true,
breadcrumb: '首页'
}
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
在 src 下创建 views 文件夹,并分配好页面。
src/App.vue:根容器,只放<router-view />和全局动画。src/views/Dashboard.vue:原本的 TaskHub 核心功能(任务列表、输入框等)。src/views/Login.vue:新增的登录页面。

改造 App.vue (变成干净的容器)
实例
<template>
<div class="min-h-screen bg-slate-50">
<router-view v-slot="{ Component }">
<transition name="page" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</div>
</template>
<style>
/* 页面切换的淡入淡出效果 */
.page-enter-active, .page-leave-active {
transition: opacity 0.2s ease;
}
.page-enter-from, .page-leave-to {
opacity: 0;
}
</style>
<div class="min-h-screen bg-slate-50">
<router-view v-slot="{ Component }">
<transition name="page" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</div>
</template>
<style>
/* 页面切换的淡入淡出效果 */
.page-enter-active, .page-leave-active {
transition: opacity 0.2s ease;
}
.page-enter-from, .page-leave-to {
opacity: 0;
}
</style>
把原本的逻辑迁入 Dashboard.vue
把之前在 App.vue 里写的那些代码(引入 Store、引入组件、模板布局)全部剪切到 src/views/Dashboard.vue 中。
Dashboard.vue 文件代码
<script setup>
import { storeToRefs } from 'pinia'
import { useTaskStore } from '@/stores/taskStore'
import { useRouter } from 'vue-router' // 引入路由
import TaskHeader from '@/components/TaskHeader.vue'
import TaskInput from '@/components/TaskInput.vue'
import TaskFilter from '@/components/TaskFilter.vue'
import TaskItem from '@/components/TaskItem.vue'
const taskStore = useTaskStore()
const router = useRouter()
const { filter, filteredTasks } = storeToRefs(taskStore)
const { addTask, removeTask, toggleTask } = taskStore
// 退出登录方法
const handleLogout = () => {
localStorage.removeItem('isLoggedIn')
router.push('/login')
}
</script>
<template>
<div class="py-12 px-4">
<div class="max-w-md mx-auto bg-white rounded-3xl shadow-xl border border-slate-100 overflow-hidden">
<TaskHeader />
<main class="p-6">
<TaskInput @add-task="addTask" />
<TaskFilter v-model="filter" />
<ul class="space-y-3">
<TransitionGroup name="list">
<TaskItem
v-for="task in filteredTasks"
:key="task.id"
:task="task"
@toggle="toggleTask"
@remove="removeTask"
/>
</TransitionGroup>
</ul>
<button
@click="handleLogout"
class="mt-8 w-full py-2 text-xs text-slate-400 hover:text-red-500 transition-colors"
>
退出当前账号
</button>
</main>
</div>
</div>
</template>
import { storeToRefs } from 'pinia'
import { useTaskStore } from '@/stores/taskStore'
import { useRouter } from 'vue-router' // 引入路由
import TaskHeader from '@/components/TaskHeader.vue'
import TaskInput from '@/components/TaskInput.vue'
import TaskFilter from '@/components/TaskFilter.vue'
import TaskItem from '@/components/TaskItem.vue'
const taskStore = useTaskStore()
const router = useRouter()
const { filter, filteredTasks } = storeToRefs(taskStore)
const { addTask, removeTask, toggleTask } = taskStore
// 退出登录方法
const handleLogout = () => {
localStorage.removeItem('isLoggedIn')
router.push('/login')
}
</script>
<template>
<div class="py-12 px-4">
<div class="max-w-md mx-auto bg-white rounded-3xl shadow-xl border border-slate-100 overflow-hidden">
<TaskHeader />
<main class="p-6">
<TaskInput @add-task="addTask" />
<TaskFilter v-model="filter" />
<ul class="space-y-3">
<TransitionGroup name="list">
<TaskItem
v-for="task in filteredTasks"
:key="task.id"
:task="task"
@toggle="toggleTask"
@remove="removeTask"
/>
</TransitionGroup>
</ul>
<button
@click="handleLogout"
class="mt-8 w-full py-2 text-xs text-slate-400 hover:text-red-500 transition-colors"
>
退出当前账号
</button>
</main>
</div>
</div>
</template>
登录逻辑实现 (Login.vue)
利用 Tailwind v4 快速构建一个极简登录页,并模拟登录行为。
实例
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const isLoading = ref(false)
const handleLogin = () => {
isLoading.value = true
// 模拟异步请求
setTimeout(() => {
localStorage.setItem('isLoggedIn', 'true')
router.push('/') // 登录成功跳转首页
isLoading.value = false
}, 1000)
}
</script>
<template>
<div class="min-h-screen flex items-center justify-center bg-slate-50">
<div class="p-8 bg-white rounded-3xl shadow-xl w-full max-w-sm border border-slate-100">
<h2 class="text-2xl font-black mb-6 text-slate-800">欢迎回来</h2>
<button
@click="handleLogin"
:disabled="isLoading"
class="w-full bg-linear-to-r from-blue-600 to-indigo-600 text-white py-3 rounded-xl font-bold hover:opacity-90 active:scale-95 transition-all disabled:opacity-50"
>
{{ isLoading ? '登录中...' : '一键进入系统' }}
</button>
<p class="mt-4 text-center text-xs text-slate-400">测试环境:点击即可登录</p>
</div>
</div>
</template>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const isLoading = ref(false)
const handleLogin = () => {
isLoading.value = true
// 模拟异步请求
setTimeout(() => {
localStorage.setItem('isLoggedIn', 'true')
router.push('/') // 登录成功跳转首页
isLoading.value = false
}, 1000)
}
</script>
<template>
<div class="min-h-screen flex items-center justify-center bg-slate-50">
<div class="p-8 bg-white rounded-3xl shadow-xl w-full max-w-sm border border-slate-100">
<h2 class="text-2xl font-black mb-6 text-slate-800">欢迎回来</h2>
<button
@click="handleLogin"
:disabled="isLoading"
class="w-full bg-linear-to-r from-blue-600 to-indigo-600 text-white py-3 rounded-xl font-bold hover:opacity-90 active:scale-95 transition-all disabled:opacity-50"
>
{{ isLoading ? '登录中...' : '一键进入系统' }}
</button>
<p class="mt-4 text-center text-xs text-slate-400">测试环境:点击即可登录</p>
</div>
</div>
</template>
完善 router/index.js 的拦截逻辑
确保你的路由配置中已经开启了安检模式:
实例
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: { title: '登录 - TaskHub', requiresAuth: false }
},
{
path: '/',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
// 路由元信息 meta:用来存放自定义数据
meta: {
title: '我的工作台',
requiresAuth: true,
breadcrumb: '首页'
}
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// src/router/index.js (核心片段)
router.beforeEach((to, from, next) => {
const isAuthenticated = localStorage.getItem('isLoggedIn') === 'true'
// 如果去首页但没登录 -> 去登录
if (to.meta.requiresAuth && !isAuthenticated) {
next('/login')
}
// 如果已登录还想去登录页 -> 回首页
else if (to.path === '/login' && isAuthenticated) {
next('/')
}
else {
next()
}
})
export default router
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: { title: '登录 - TaskHub', requiresAuth: false }
},
{
path: '/',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
// 路由元信息 meta:用来存放自定义数据
meta: {
title: '我的工作台',
requiresAuth: true,
breadcrumb: '首页'
}
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// src/router/index.js (核心片段)
router.beforeEach((to, from, next) => {
const isAuthenticated = localStorage.getItem('isLoggedIn') === 'true'
// 如果去首页但没登录 -> 去登录
if (to.meta.requiresAuth && !isAuthenticated) {
next('/login')
}
// 如果已登录还想去登录页 -> 回首页
else if (to.path === '/login' && isAuthenticated) {
next('/')
}
else {
next()
}
})
export default router
在 main.js 中完成最后拼图
确保你的 main.js 引入并挂载了路由:
实例
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import router from './router' // 引入路由
import App from './App.vue'
import './style.css'
const app = createApp(App)
app.use(createPinia())
app.use(router) // 2. 注册路由
app.mount('#app')
import { createPinia } from 'pinia'
import router from './router' // 引入路由
import App from './App.vue'
import './style.css'
const app = createApp(App)
app.use(createPinia())
app.use(router) // 2. 注册路由
app.mount('#app')
最终效果
- 访问 /
:如果你没登录,页面会闪现一下然后跳转到/login。 - 访问 /login
:点击按钮,本地存储isLoggedIn变为true,然后瞬间切入工作台。 - 刷新页面:Pinia 会从 LocalStorage 读取任务,路由守卫会检查登录状态,一切数据都不会丢失。

登录后,可以点击退出登录:

知识点讲解
- 为什么
App.vue变空了?
在单页面应用(SPA)中,App.vue是整个应用的外壳。我们把它变空是为了能根据 URL 的变化,在其中动态地塞进不同的页面(Dashboard 或 Login)。 useRouter的实战:
在Dashboard.vue中,我们通过router.push('/login')实现了退出功能。注意:push会向浏览器历史记录添加一条新记录,点击浏览器返回键是可以回退的。- Tailwind v4 的布局继承:
由于我们在App.vue的全局容器里加了bg-slate-50和min-h-screen,所有的子页面(Dashboard、Login)都会默认继承这个浅灰色背景,保证了视觉的统一性。
